From 0041cd9660054b68656ce0534cf84ffa46e3020f Mon Sep 17 00:00:00 2001 From: OverSu Date: Thu, 8 Oct 2026 14:27:17 +0200 Subject: [PATCH 01/12] Update shared Gaia workflows and Sargo development tooling Fix camera switching and Contacts/SMS activity routing across shared hosts. Integrate Marketplace installation and native standalone recovery tools, Bluetooth runtime packages and trusted certificates. Update English/French READMEs and the shared changelog with tested scope and remaining radio/audio/Bluetooth limitations. Preview17 remains the existing APK; no new native image is released. Validation: shared JavaScript and native Python tests; Android packaging and payload parity from an isolated publication checkout. --- .gitignore | 3 + README.fr.md | 21 +- README.md | 21 +- .../org/vulpes_os/preview/GaiaServer.java | 14 +- .../org/vulpes_os/preview/InstalledApps.java | 37 + .../org/vulpes_os/preview/MainActivity.java | 31 +- .../vulpes_os/preview/VulpesApplication.java | 37 + android/extension/background.js | 63 +- android/extension/content.js | 2 +- android/extension/manifest.json | 46 +- android/extension/marketplace-content.js | 1 + android/tests/settings-codec.test.mjs | 4 +- android/tools/pack-assets.py | 7 +- .../contacts/js/activities.js | 6 +- .../contacts/js/contacts.js | 4 +- .../contacts/js/views/form.js | 2 + .../gaia_build_defer_index.js | 2 +- .../marketplace.gaiamobile.org/icon.png | Bin 0 -> 190651 bytes .../marketplace.gaiamobile.org/index.html | 5 + .../manifest.webapp | 19 + .../marketplace.css | 1 + .../marketplace.gaiamobile.org/marketplace.js | 32 + .../gaia_build_defer_manage.js | 2 +- .../gaia_build_defer_pick.js | 2 +- .../js/tone_player.js | 2 +- changelog.json | 69 ++ .../communications/contacts/js/activities.js | 6 +- .../communications/contacts/js/contacts.js | 4 +- .../communications/contacts/js/views/form.js | 2 + gaia/apps/homescreen/js/apps.js | 4 + gaia/apps/marketplace/icon.png | Bin 0 -> 190651 bytes gaia/apps/marketplace/index.html | 5 + gaia/apps/marketplace/manifest.webapp | 19 + gaia/apps/marketplace/marketplace.css | 1 + gaia/apps/marketplace/marketplace.js | 32 + gaia/apps/ringtones/js/tone_player.js | 2 +- .../settings/js/modules/page_transitions.js | 48 +- gaia/apps/system/js/system_banner.js | 49 +- host/Runtime.sys.mjs | 58 +- host/Views.sys.mjs | 64 +- host/actors/MarketplaceChild.sys.mjs | 7 + host/actors/MarketplaceParent.sys.mjs | 13 + host/actors/VulpesChild.sys.mjs | 21 + host/desktop.css | 4 + host/gaia-camera.js | 199 +++--- host/gaia-compat.js | 70 +- host/gaia-restoration.js | 10 +- host/gaia-wifi.js | 4 +- host/marketplace-page.js | 14 + host/settings-codec.js | 41 ++ host/shell.js | 11 +- overrides/settings.gaiamobile.org/js/main.js | 238 +++++++ .../resources/branding/vulpes-symbol.png | Bin 0 -> 138726 bytes .../system.gaiamobile.org/js/system_banner.js | 203 ++++++ release.json | 6 +- services/activities.mjs | 114 ++++ services/packages.mjs | 77 +++ tests/ProbeChild.sys.mjs | 11 + tests/activities.test.mjs | 83 +++ tests/android-sms-handoff.test.mjs | 38 ++ tests/camera-lifecycle.test.mjs | 152 +++++ tests/contacts.test.mjs | 59 ++ tests/installed_apps_test.py | 28 + tests/packages.test.mjs | 35 + tests/settings-codec.test.mjs | 35 + tests/settings-transitions.test.mjs | 29 + tests/wifi-throttle.test.mjs | 24 + tools/check-parity.py | 32 +- tools/installed_apps.py | 72 ++ tools/project.py | 9 + tools/refresh-settings-panels.py | 16 +- tools/serve-gaia.py | 52 +- tools/test-camera.py | 7 +- tools/test-contacts.py | 183 +++++ tools/test-system-banner.py | 38 ++ tundra/boot/sparse-backup.c | 46 ++ tundra/boot/standalone-mount.sh | 14 + tundra/droidian/probe.sys.mjs | 643 +++++++++++------- tundra/manifests/sargo-hardware.json | 22 + tundra/runtime/hal_policy.py | 2 +- tundra/runtime/mark-boot-success.py | 38 ++ tundra/runtime/prepare-bluetooth.py | 34 + tundra/runtime/prepare-hardware.py | 3 + tundra/runtime/runtime-packages.py | 82 +++ tundra/runtime/setup-hal.py | 19 +- tundra/tests/test_hardware_status.py | 6 +- tundra/tests/test_runtime_packages.py | 47 ++ tundra/tests/test_sparse_backup.py | 37 + tundra/tests/test_standalone_boot.py | 91 +++ tundra/tests/test_standalone_flash.py | 80 +++ tundra/tests/test_standalone_restore.py | 69 ++ tundra/tools/build-rescue-boot.py | 10 + tundra/tools/build-standalone-boot.py | 146 ++++ tundra/tools/build-standalone-volume.py | 83 +++ tundra/tools/build-system-image.py | 2 +- tundra/tools/flash-standalone-sargo.py | 116 ++++ tundra/tools/install-sargo.py | 2 + tundra/tools/package-standalone-sargo.py | 63 ++ tundra/tools/restore-standalone-sargo.py | 83 +++ tundra/tools/sparse_backup.py | 25 + tundra/tools/standalone_volume.py | 35 + 101 files changed, 4004 insertions(+), 456 deletions(-) create mode 100644 android/app/src/main/java/org/vulpes_os/preview/InstalledApps.java create mode 100644 android/extension/marketplace-content.js create mode 100644 assets/webapps/marketplace.gaiamobile.org/icon.png create mode 100644 assets/webapps/marketplace.gaiamobile.org/index.html create mode 100644 assets/webapps/marketplace.gaiamobile.org/manifest.webapp create mode 100644 assets/webapps/marketplace.gaiamobile.org/marketplace.css create mode 100644 assets/webapps/marketplace.gaiamobile.org/marketplace.js create mode 100644 gaia/apps/marketplace/icon.png create mode 100644 gaia/apps/marketplace/index.html create mode 100644 gaia/apps/marketplace/manifest.webapp create mode 100644 gaia/apps/marketplace/marketplace.css create mode 100644 gaia/apps/marketplace/marketplace.js create mode 100644 host/actors/MarketplaceChild.sys.mjs create mode 100644 host/actors/MarketplaceParent.sys.mjs create mode 100644 host/marketplace-page.js create mode 100644 host/settings-codec.js create mode 100644 overrides/settings.gaiamobile.org/js/main.js create mode 100644 overrides/shared/resources/branding/vulpes-symbol.png create mode 100755 overrides/system.gaiamobile.org/js/system_banner.js create mode 100644 services/activities.mjs create mode 100644 services/packages.mjs create mode 100644 tests/activities.test.mjs create mode 100644 tests/android-sms-handoff.test.mjs create mode 100644 tests/camera-lifecycle.test.mjs create mode 100644 tests/contacts.test.mjs create mode 100644 tests/installed_apps_test.py create mode 100644 tests/packages.test.mjs create mode 100644 tests/settings-codec.test.mjs create mode 100644 tests/settings-transitions.test.mjs create mode 100644 tools/installed_apps.py create mode 100644 tools/test-contacts.py create mode 100644 tools/test-system-banner.py create mode 100644 tundra/boot/sparse-backup.c create mode 100644 tundra/boot/standalone-mount.sh create mode 100644 tundra/manifests/sargo-hardware.json create mode 100644 tundra/runtime/mark-boot-success.py create mode 100644 tundra/runtime/prepare-bluetooth.py create mode 100644 tundra/runtime/runtime-packages.py create mode 100644 tundra/tests/test_runtime_packages.py create mode 100644 tundra/tests/test_sparse_backup.py create mode 100644 tundra/tests/test_standalone_boot.py create mode 100644 tundra/tests/test_standalone_flash.py create mode 100644 tundra/tests/test_standalone_restore.py create mode 100644 tundra/tools/build-standalone-boot.py create mode 100644 tundra/tools/build-standalone-volume.py create mode 100644 tundra/tools/flash-standalone-sargo.py create mode 100644 tundra/tools/package-standalone-sargo.py create mode 100644 tundra/tools/restore-standalone-sargo.py create mode 100644 tundra/tools/sparse_backup.py create mode 100644 tundra/tools/standalone_volume.py diff --git a/.gitignore b/.gitignore index ac57c53e..06812952 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ __pycache__/ # Old B2G registry is unused by the modern host and contains historical paths. /assets/webapps/webapps.json + +# Local issue tracking and roadmap; not included in publication. +/tickets/ diff --git a/README.fr.md b/README.fr.md index 1d2b8e5c..455b266c 100644 --- a/README.fr.md +++ b/README.fr.md @@ -3,7 +3,7 @@

Vulpes OS

L'esprit de Firefox OS. Un Gecko moderne. Une base Gaia commune.
Un projet communautaire pour retrouver une expérience mobile fondée sur le Web, sur PC, Android et téléphone natif.

-Vulpes Preview 15 +Vulpes Preview 17 Gecko bureau 156.0.1 Expérimental

@@ -26,9 +26,9 @@ Ce dépôt contient désormais l'interface commune, l'hôte PC, l'application An | **APK Android** | Preview installable ; Android et ses permissions restent en dessous | GeckoView **156.0** | | **Pixel 3a · Sargo** | Sources Tundra ; build natif de développement testé sur notre appareil | Gecko **156.0.1** ARM64 | -### Android — Preview 15 +### Android — Preview 17 -Téléchargez l'APK et `SHA256SUMS.txt` depuis [la release Preview 15](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15). Une installation par-dessus une ancienne Preview Vulpes signée conserve ses données. +Téléchargez l'APK et `SHA256SUMS.txt` depuis [la release Preview 17](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.17). Une installation par-dessus une ancienne Preview Vulpes signée conserve ses données. - Minimum déclaré : **Android 8 / API 26**. Cible : **API 37**. - APK universel : **ARM64, ARMv7 et x86_64** ; environ **600 Mio**, moteur GeckoView inclus. @@ -56,9 +56,20 @@ cd VulpesOS Notre image de développement Sargo installée a passé les contrôles de démarrage et de persistance. Des appels réels et des SMS dans les deux sens ont été observés ; les derniers essais couvrent les conversations, les bascules de capteur photo, la capture JPEG pleine résolution, le focus tactile, le zoom numérique et l'arrêt du vibreur. -**Cette release ne contient pas d'image générique à flasher.** Le boot privé dépend encore de la disposition du stockage du téléphone inventorié, d'images système/données déjà déployées et d'accès de maintenance. Publier ce fichier ne fournirait pas une installation fonctionnelle à un autre Pixel. Les recettes Tundra sont incluses pour les contributeurs ; elles nécessitent un inventaire, des sauvegardes vérifiées et des composants matériels fournis localement. Ne réutilisez pas la sauvegarde d'un autre appareil et ne considérez pas un boot isolé comme une ROM complète. +**Une installation autonome est maintenant testée localement.** Le paquet Fastboot fournit le système, un espace de données vierge et les composants matériels associés au noyau. Il remplace `userdata`, `boot_a`, `dtbo_a` et `vbmeta_a` : il n’utilise plus les fichiers d’une ancienne installation Droidian et ne nécessite pas de session Android ni d’ADB. Deux démarrages depuis le stockage interne ont passé chacun onze contrôles Gaia et dix contrôles natifs, avec conservation des réglages. -Les outils de préparation prennent désormais un dossier privé par appareil : inventaire, sauvegardes, construction RAM, transfert SSH et secours. La procédure reste réservée au noyau qualifié et à un stockage ext4/LVM compatible ; elle ne permet pas encore de partir directement d’Android d’origine. +L’installation demande un **Pixel 3a (Sargo) déverrouillé** et efface toutes les données de `userdata`. Le bootloader et les calibrations propres au téléphone restent en place. Ce candidat réserve actuellement 8 Gio au volume, dont 1 Gio de données modifiables ; l’extension automatique n’est pas encore implémentée. Il a été testé sur notre Pixel, pas sur chaque combinaison de firmware existante. + +**Le paquet autonome n’est pas encore proposé en téléchargement dans cette release.** Les recettes et l’installateur sont préparés ; la revue de provenance et de redistribution des composants matériels reste à terminer. Les sauvegardes et les clés de maintenance du téléphone de développement ne font pas partie du paquet générique. + +## 🛠️ Dernières évolutions des sources · 8 octobre 2026 + +- **Photo :** bascule des capteurs séquencée, libération fiable et transport des images corrigé. Capture avant/arrière, focus, zoom et ouverture dans Galerie testés sur notre Pixel 3a. +- **Contacts :** fiche actualisée après édition, parcours SMS/sélecteur/Téléphone rétablis et sélection d’une photo dans Galerie. Les tests PC couvrent recherche, recadrage, favoris et suppression ; les tests natifs couvrent création, édition et navigation entre applications. +- **Marketplace :** installation d’applications intégrée, permissions restreintes et retour contrôlé vers Gaia. +- **Tundra :** outils de build autonome et de restauration, paquets Bluetooth verrouillés et certificats natifs. La découverte matérielle Bluetooth fonctionne ; l’appairage Gaia et les profils restent à terminer. + +**Ces changements de sources sont postérieurs à l’APK Preview 17 publié.** Les ressources Android sont préparées, mais aucun nouvel APK ni image native contenant tous ces correctifs n’est publié. Le dernier candidat natif est testé en RAM ; l’enregistrement cellulaire reste en cours d’analyse et l’audio des appels dans les deux sens n’est pas encore qualifié. L’historique commun PC/Android/Tundra se trouve dans [`changelog.json`](changelog.json). ## ✨ Ce que nous partageons diff --git a/README.md b/README.md index f21ba9c7..62c3a23a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

Vulpes OS

The spirit of Firefox OS. A modern Gecko. One shared Gaia.
A community project bringing a web-based phone experience to desktop, Android and native hardware.

-Vulpes Preview 15 +Vulpes Preview 17 Desktop Gecko 156.0.1 Experimental

@@ -26,9 +26,9 @@ This repository now contains the shared interface, desktop host, Android applica | **Android APK** | Installable Preview; keeps Android and its permissions underneath | GeckoView **156.0** | | **Pixel 3a · Sargo** | Tundra sources; native development build tested on our device | Gecko **156.0.1** ARM64 | -### Android — Preview 15 +### Android — Preview 17 -Download the APK and `SHA256SUMS.txt` from [the Preview 15 release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15). Installing over an earlier signed Vulpes Preview preserves its data. +Download the APK and `SHA256SUMS.txt` from [the Preview 17 release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.17). Installing over an earlier signed Vulpes Preview preserves its data. - Minimum declared: **Android 8 / API 26**. Target: **API 37**. - Universal APK: **ARM64, ARMv7 and x86_64**; approximately **600 MiB** including GeckoView. @@ -56,9 +56,20 @@ cd VulpesOS Our installed Sargo development image has passed startup and persistence checks. Real calls and two-way SMS have been observed; the latest checks cover conversation rendering, camera sensor switching, full-resolution JPEG capture, touch focus, digital zoom and bounded vibration. -**A generic flashable image is not included in this release.** The current private boot depends on the inventoried phone's storage layout, staged system/data images and maintenance credentials. Publishing that file would not give another Pixel a working installation. Tundra's source recipes are included for contributors; they require device inventory, verified backups and locally supplied hardware components. Do not use another device's backup or treat a boot image as a complete ROM. +**A standalone installation has now been tested locally.** The Fastboot package provides the system, fresh writable storage and the hardware components paired with the kernel. It replaces `userdata`, `boot_a`, `dtbo_a` and `vbmeta_a`: startup no longer uses files from an old Droidian installation and requires neither an Android session nor ADB. Two boots from internal storage passed eleven Gaia and ten native checks each and retained settings. -Preparation tools now accept a private workspace per device: inventory, backups, RAM builds, SSH transfers and recovery. The procedure remains limited to the qualified kernel and compatible ext4/LVM storage; it does not yet install directly from stock Android. +Installation requires an **unlocked Pixel 3a (Sargo)** and erases all existing `userdata`. The bootloader and device-specific calibration remain in place. This candidate currently reserves an 8 GiB volume, including 1 GiB of writable data; automatic expansion is not implemented yet. It has been tested on our Pixel, not every existing firmware combination. + +**The standalone package is not yet available as a download in this release.** Build recipes and the installer are prepared; hardware component provenance and redistribution review are still pending. Development-phone backups and maintenance keys are excluded from the generic package. + +## 🛠️ Latest source improvements · 8 October 2026 + +- **Camera:** serialized sensor switching, reliable release and binary photo transport. Front/rear capture, focus, zoom and Gallery opening tested on our Pixel 3a. +- **Contacts:** refreshed details after editing, restored SMS/number-picker/dialer flows, and Gallery photo selection. Desktop tests cover search, cropping, favourites and deletion; native tests cover creation, editing and cross-app navigation. +- **Marketplace:** integrated application installation with permission restrictions and controlled return to Gaia. +- **Tundra:** standalone build/recovery tools, pinned Bluetooth runtime packages and native trusted certificates. Bluetooth hardware discovery works; Gaia pairing and profiles remain unfinished. + +**These are source updates, newer than the published Preview 17 APK.** Updated Android assets are prepared, but no new APK or native image containing all these fixes is published. The latest native candidate is tested in RAM; cellular registration remains under investigation and two-way call audio is not yet qualified. See [`changelog.json`](changelog.json) for the shared desktop/Android/Tundra history. ## ✨ What is shared diff --git a/android/app/src/main/java/org/vulpes_os/preview/GaiaServer.java b/android/app/src/main/java/org/vulpes_os/preview/GaiaServer.java index 6ac6e23b..4319721d 100644 --- a/android/app/src/main/java/org/vulpes_os/preview/GaiaServer.java +++ b/android/app/src/main/java/org/vulpes_os/preview/GaiaServer.java @@ -14,11 +14,13 @@ import java.util.zip.*; final class GaiaServer implements Closeable { static final int PORT = 18765; + final InstalledApps installed; private final ZipFile zip; private final ServerSocket socket; private final ExecutorService workers = Executors.newFixedThreadPool(4); GaiaServer(Context context) throws IOException { + try {installed=new InstalledApps(context);}catch(Exception e){throw new IOException(e);} File archive = new File(context.getCacheDir(), "gaia.zip"); // Atomic refresh on every process start; the APK is the source of truth. File temp = new File(context.getCacheDir(), "gaia.zip.tmp"); @@ -94,7 +96,7 @@ final class GaiaServer implements Closeable { } String app = host.substring(0, host.indexOf('.')); if (app.equals("theme")) app = "default_theme"; - if (zip.getEntry(app + "/manifest.webapp") == null) { + if (!InstalledApps.valid(app) && zip.getEntry(app + "/manifest.webapp") == null) { error(out, 404); return; } @@ -109,6 +111,16 @@ final class GaiaServer implements Closeable { return; } if (path.endsWith("/")) path += "index.html"; + if (InstalledApps.valid(app)) { + byte[] body=installed.read(app,path.substring(1)); + if(body==null){error(out,404);return;} + String ext=path.substring(path.lastIndexOf('.')+1).toLowerCase(Locale.ROOT); + String mime=MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext); + if(ext.equals("js"))mime="text/javascript"; + if(ext.equals("webapp"))mime="application/json"; + if(mime==null)mime="application/octet-stream"; + out.write(("HTTP/1.1 200 OK\r\nContent-Type: "+mime+"\r\nContent-Length: "+body.length+"\r\nConnection: close\r\nX-Content-Type-Options: nosniff\r\nCache-Control: no-store\r\nAccess-Control-Allow-Origin: *\r\nContent-Security-Policy: object-src 'none'; frame-ancestors http://*.localhost:"+PORT+"\r\n\r\n").getBytes(StandardCharsets.US_ASCII));out.write(body);return; + } String key = path.startsWith("/_vulpes/") ? path.substring(1) : app + path; ZipEntry entry = zip.getEntry(key); if (entry == null && path.startsWith("/shared/")) entry = zip.getEntry( diff --git a/android/app/src/main/java/org/vulpes_os/preview/InstalledApps.java b/android/app/src/main/java/org/vulpes_os/preview/InstalledApps.java new file mode 100644 index 00000000..8ecd8840 --- /dev/null +++ b/android/app/src/main/java/org/vulpes_os/preview/InstalledApps.java @@ -0,0 +1,37 @@ +package org.vulpes_os.preview; + +import android.content.Context; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.*; +import org.json.*; + +final class InstalledApps { + private final File root; + private final Map 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); + } +} diff --git a/android/app/src/main/java/org/vulpes_os/preview/MainActivity.java b/android/app/src/main/java/org/vulpes_os/preview/MainActivity.java index 221e8f0c..5b4dcba7 100644 --- a/android/app/src/main/java/org/vulpes_os/preview/MainActivity.java +++ b/android/app/src/main/java/org/vulpes_os/preview/MainActivity.java @@ -45,8 +45,7 @@ public final class MainActivity extends Activity { root.setOnApplyWindowInsetsListener((v, insets) -> { if (Build.VERSION.SDK_INT >= 30) { android.graphics.Insets bars = insets.getInsets( - WindowInsets.Type.systemBars() | - WindowInsets.Type.ime() | + WindowInsets.Type.ime() | WindowInsets.Type.displayCutout() ); v.setPadding(bars.left, bars.top, bars.right, bars.bottom); @@ -54,11 +53,12 @@ public final class MainActivity extends Activity { insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), - insets.getSystemWindowInsetBottom() + insets.getSystemWindowInsetBottom() > dp(100) ? insets.getSystemWindowInsetBottom() : 0 ); return insets; }); setContentView(root); + immersive(); // Respect Android's screen timeout; Gaia must not keep the phone awake indefinitely. float brightness = app.getSharedPreferences("android", 0).getFloat("brightness", -1); if (brightness >= 0) { @@ -166,6 +166,28 @@ public final class MainActivity extends Activity { } } + private void immersive() { + if (Build.VERSION.SDK_INT >= 30) { + getWindow().setDecorFitsSystemWindows(false); + WindowInsetsController controller = getWindow().getInsetsController(); + if (controller != null) { + controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + controller.hide(WindowInsets.Type.systemBars()); + } + } else { + getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + } + } + + @Override + public void onWindowFocusChanged(boolean focused) { + super.onWindowFocusChanged(focused); + if (focused) immersive(); + } + private int dp(int value) { return Math.round(value * getResources().getDisplayMetrics().density); } @@ -274,7 +296,7 @@ public final class MainActivity extends Activity { setHomeSurface(app.homeVisible); } - private void closeBrowser() { + void closeBrowser() { webPanel.setVisibility(View.GONE); gaiaView.setVisibility(View.VISIBLE); setHomeSurface(app.homeVisible); @@ -356,6 +378,7 @@ public final class MainActivity extends Activity { public void onResume() { super.onResume(); resumed = true; + immersive(); app.activity = this; if (recoverGaia) { recoverGaia = false; diff --git a/android/app/src/main/java/org/vulpes_os/preview/VulpesApplication.java b/android/app/src/main/java/org/vulpes_os/preview/VulpesApplication.java index d9baa910..3a46db7e 100644 --- a/android/app/src/main/java/org/vulpes_os/preview/VulpesApplication.java +++ b/android/app/src/main/java/org/vulpes_os/preview/VulpesApplication.java @@ -17,9 +17,32 @@ public final class VulpesApplication extends Application { GeckoSession gaiaSession; AndroidServices services; AndroidNotifications notifications; + boolean pendingScreenLock; boolean homeVisible; boolean homeAtBottom; + @Override + public void onCreate() { + super.onCreate(); + android.content.BroadcastReceiver receiver = new android.content.BroadcastReceiver() { + @Override public void onReceive(android.content.Context context, android.content.Intent intent) { + pendingScreenLock = true; + sendScreenLock(); + } + }; + android.content.IntentFilter filter = new android.content.IntentFilter(android.content.Intent.ACTION_SCREEN_OFF); + if (android.os.Build.VERSION.SDK_INT >= 33) registerReceiver(receiver,filter,RECEIVER_NOT_EXPORTED); + else registerReceiver(receiver,filter); + } + + void sendScreenLock() { + if (port == null || !pendingScreenLock) return; + try { + port.postMessage(new JSONObject().put("type","android-screen-off")); + pendingScreenLock = false; + } catch (Exception error) {Log.w("Vulpes","Screen lock pending",error);} + } + void prepare() throws IOException { if (runtime != null) return; server = new GaiaServer(this); @@ -58,6 +81,7 @@ public final class VulpesApplication extends Application { @Override public void onConnect(WebExtension.Port p) { port = p; + sendScreenLock(); if (activity != null) notifications.click(activity.getIntent()); p.setDelegate( new WebExtension.PortDelegate() { @@ -80,6 +104,18 @@ public final class VulpesApplication extends Application { return GeckoResult.fromException(new SecurityException("EXTENSION_REQUIRED")); } JSONObject json = (JSONObject) message; + if (json.optString("type").equals("marketplace.return") && activity!=null) {activity.runOnUiThread(()->activity.closeBrowser());return GeckoResult.fromValue(null);} + if (json.optString("type").startsWith("packages.")) { + GeckoResult 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); @@ -124,6 +160,7 @@ public final class VulpesApplication extends Application { } void resumed() { + sendScreenLock(); if (port == null) return; try { port.postMessage(new JSONObject().put("type", "android-resume")); diff --git a/android/extension/background.js b/android/extension/background.js index c08d901a..e189b1d5 100644 --- a/android/extension/background.js +++ b/android/extension/background.js @@ -1,3 +1,5 @@ +import { Packages, packageURL } from './services/packages.mjs'; +import { Activities, isGaiaActivity } from './services/activities.mjs'; import { authorizePlatform } from './services/platform.mjs'; import { SettingsService } from './services/settings.mjs'; import { DataStoreService } from './services/datastores.mjs'; @@ -7,6 +9,9 @@ import { Messages } from './Messages.mjs'; import { Connections } from './Connections.mjs'; import { Alarms } from './Alarms.mjs'; const peers = new Set(); +let activities, packages; +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; const identity = (peer) => peer.app; function notify(peer, type, data) { @@ -26,7 +31,7 @@ function grants(app) { function broadcast(type, data, target) { for (const peer of peers) { if (target && peer.app.id !== target) continue; - if (type === 'settingchange' && !grants(peer.app).includes('settings.read')) continue; + if (type === 'settingchange' && data.key !== 'language.current' && !grants(peer.app).includes('settings.read')) continue; if ( type === 'contactchange' && !peer.app.manifest.permissions?.contacts?.access?.includes('read') @@ -45,7 +50,7 @@ function broadcast(type, data, target) { notify(peer, type, data); } } -function launch(target, entryPoint, path) { +function launch(target, entryPoint, path, reload = false) { path ||= target.manifest.entry_points?.[entryPoint]?.launch_path || target.manifest.launch_path || @@ -53,7 +58,7 @@ function launch(target, entryPoint, path) { const url = new URL(path, target.origin); if (url.origin !== target.origin) throw Error('INVALID_URL'); broadcast( - 'launch', + reload ? 'activity-launch' : 'launch', { manifestURL: target.manifestURL, origin: target.origin, @@ -66,6 +71,8 @@ function launch(target, entryPoint, path) { } const ready = (async () => { apps = await (await fetch('registry.json')).json(); + await refreshInstalled(); + packages=new Packages({save:item=>nativePackages('save',{item}),changed:refreshInstalled}); const defaults = await (await fetch('defaults.json')).json(); const saved = await browser.storage.local.get('settings'); values = { ...defaults, ...saved.settings }; @@ -82,6 +89,8 @@ const ready = (async () => { values['wallpaper.image.valid'] = false; await browser.storage.local.set({ settings: values }); } + activities = new Activities({identity,apps:()=>apps,notify,uuid:()=>crypto.randomUUID(), + launch:(target,path,{reload=false,entryPoint}={})=>launch(target,entryPoint,path,reload)}); let writes = Promise.resolve(); settings = new SettingsService( { @@ -123,6 +132,12 @@ const ready = (async () => { // Java owns this port. Web pages have no access to native messaging. let native; 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; @@ -140,7 +155,15 @@ function connectNative() { native.onMessage.addListener(async (message) => { await ready; if (['home', 'holdhome'].includes(message.type)) broadcast(message.type, {}, 'system'); - if (message.type === 'android-resume') await refreshAndroid().catch(console.warn); + if (message.type === 'android-screen-off') { + pendingScreenLock = true; + deliverScreenLock(); + } + if (message.type === 'android-resume') { + deliverScreenLock(); + broadcast('screen-on', {}, 'system'); + await refreshAndroid().catch(console.warn); + } if (message.type === 'notification-click') { pendingNotification = message.data; deliverNotification(); @@ -206,6 +229,7 @@ async function handle(peer, request) { if (method === 'android.systemReady') { if (app.id !== 'system') throw Error('PERMISSION_DENIED'); systemReady = true; + deliverScreenLock(); deliverNotification(); return null; } @@ -278,6 +302,7 @@ async function handle(peer, request) { } if (!Object.keys(params.values).length) return null; } + if (method === 'locale.current') return values['language.current'] || 'en-US'; if (method === 'settings.snapshot') { if (!grants(app).includes('settings.read')) throw Error('PERMISSION_DENIED'); return { ...values }; @@ -287,6 +312,14 @@ async function handle(peer, request) { if (reply.error) throw Error(reply.error.code); return reply.result; } + if(method.startsWith('marketplace.')) { + if(app.id!=='marketplace')throw Error('PERMISSION_DENIED'); + if(method==='marketplace.poll'){const url=marketplaceDownload;marketplaceDownload=null;return url;} + if(method==='marketplace.cancel'){packages.cancel();return true;} + if(method==='marketplace.prepare')return packages.prepare(params.url); + if(method==='marketplace.install'){const id=await packages.install(params.token);const installed=apps.find(a=>a.id===id);broadcast('apps-installed',installed);return installed;} + throw Error('METHOD_NOT_SUPPORTED'); + } if (method === 'apps.self') { console.info('VULPES app ready', app.id); return app; @@ -298,6 +331,10 @@ async function handle(peer, request) { if (!manage) throw Error('PERMISSION_DENIED'); return apps; } + if(method==='apps.uninstall') { + const installed=apps.find(a=>a.id===params.id && a.installed);if(!manage || !installed)throw Error('PERMISSION_DENIED'); + await nativePackages('remove',{id:params.id});await refreshInstalled();broadcast('apps-uninstalled',installed);return true; + } if (method === 'apps.launch') { const target = apps.find((a) => a.manifestURL === params.manifestURL); if (!target || (!manage && app.id !== target.id)) throw Error('PERMISSION_DENIED'); @@ -319,9 +356,13 @@ async function handle(peer, request) { connections.close(peer, params.id); return null; } + if (method === 'activities.cancel') return activities.cancel(peer); + if (method === 'activities.subscribe') return activities.subscribe(peer); + if (method === 'activities.finish') return activities.finish(peer,params); + if (method === 'activities.start' && isGaiaActivity(params)) return activities.start(peer,params); if (method === 'activities.start') { if (params.name === 'record' && params.data?.type === 'photos') { - if (app.id === 'gallery') { + if (['gallery', 'system'].includes(app.id)) { const camera = apps.find((a) => a.id === 'camera'); if (!camera) throw Error('ACTIVITY_NOT_SUPPORTED'); launch(camera); @@ -430,9 +471,21 @@ browser.runtime.onConnect.addListener((port) => { peers.delete(peer); 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; +}); diff --git a/android/extension/content.js b/android/extension/content.js index ad150b58..17bbcc85 100644 --- a/android/extension/content.js +++ b/android/extension/content.js @@ -1,6 +1,6 @@ /* Extension world: the browser provides origin/tab/frame identity, never Gaia. */ (() => { - if (location.port !== '18765') return; + if (location.port !== '18765' || location.hostname.startsWith('user-')) return; const win = window.wrappedJSObject; console.info('VULPES Android bridge', location.origin); let port; diff --git a/android/extension/manifest.json b/android/extension/manifest.json index b5ec06a8..3220ceb0 100644 --- a/android/extension/manifest.json +++ b/android/extension/manifest.json @@ -1 +1,45 @@ -{"manifest_version":2,"name":"Vulpes Android host","version":"1.1","browser_specific_settings":{"gecko":{"id":"android-host@vulpes-os.org"}},"permissions":["storage","unlimitedStorage","nativeMessaging","geckoViewAddons","http://*.localhost/*"],"background":{"page":"background.html","persistent":true},"content_scripts":[{"matches":["http://*.localhost/*"],"js":["content.js"],"all_frames":true,"run_at":"document_start"}]} +{ + "manifest_version": 2, + "name": "Vulpes Android host", + "version": "1.1", + "browser_specific_settings": { + "gecko": { + "id": "android-host@vulpes-os.org" + } + }, + "permissions": [ + "storage", + "unlimitedStorage", + "nativeMessaging", + "geckoViewAddons", + "http://*.localhost/*", + "https://vulpes-os.org/*" + ], + "background": { + "page": "background.html", + "persistent": true + }, + "content_scripts": [ + { + "matches": [ + "http://*.localhost/*" + ], + "js": [ + "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" + } + ] +} diff --git a/android/extension/marketplace-content.js b/android/extension/marketplace-content.js new file mode 100644 index 00000000..496dd521 --- /dev/null +++ b/android/extension/marketplace-content.js @@ -0,0 +1 @@ +installMarketplacePage(window,url=>browser.runtime.sendMessage({marketplaceDownload:url}).catch(console.error)); diff --git a/android/tests/settings-codec.test.mjs b/android/tests/settings-codec.test.mjs index ce863a0b..e43eec1f 100644 --- a/android/tests/settings-codec.test.mjs +++ b/android/tests/settings-codec.test.mjs @@ -5,10 +5,10 @@ import { SettingsService } from '../../services/settings.mjs'; const context = vm.createContext({ window: {}, Blob, Uint8Array, btoa, atob }); vm.runInContext( - await readFile(new URL('../ui/settings-codec.js', import.meta.url), 'utf8'), + await readFile(new URL('../../host/settings-codec.js', import.meta.url), 'utf8'), context, ); -const { encode, decode } = context.window.VulpesAndroidSettingsCodec; +const { encode, decode } = context.window.VulpesSettingsCodec; // A binary wallpaper larger than the original 1 MiB JSON ceiling, including // every byte value. Exercise the actual service, JSON storage and observer path. const pixels = Uint8Array.from({ length: 1200000 }, (_, i) => i % 256); diff --git a/android/tools/pack-assets.py b/android/tools/pack-assets.py index e3af8394..e6c9e1ed 100644 --- a/android/tools/pack-assets.py +++ b/android/tools/pack-assets.py @@ -12,7 +12,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "tools")) -from project import build_info, validate_assets, release_info +from project import build_info, validate_assets, release_info, application_manifest validate_assets() OUT = ROOT / "android/app/src/main/assets" @@ -39,7 +39,7 @@ for manifest in sorted(APPS.glob("*/manifest.webapp")): id=name, origin=origin, manifestURL=origin + "/manifest.webapp", - manifest=json.loads(translate(manifest.read_text())), + manifest=application_manifest(name, json.loads(translate(manifest.read_text())), "--include-dev-apps" in sys.argv), ) ) info = build_info() @@ -126,7 +126,7 @@ if "--bridge-only" not in sys.argv or not (OUT / "gaia.zip").exists(): .read_text() ) put("_vulpes/" + name, translate(text).encode()) - put("_vulpes/settings-codec.js", (ROOT / "android/ui/settings-codec.js").read_bytes()) + put("_vulpes/settings-codec.js", (ROOT / "host/settings-codec.js").read_bytes()) put("_vulpes/android-ui.js", (ROOT / "android/ui/android-ui.js").read_bytes()) put( "_vulpes/android.css", @@ -145,6 +145,7 @@ for name in ["Connections", "Contacts", "Messages"]: ) (ext / f"{name}.mjs").write_text(text) shutil.copy2(ROOT / "host/Alarms.sys.mjs", ext / "Alarms.mjs") +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())) diff --git a/assets/webapps/communications.gaiamobile.org/contacts/js/activities.js b/assets/webapps/communications.gaiamobile.org/contacts/js/activities.js index 12cb2a1f..84e333b1 100644 --- a/assets/webapps/communications.gaiamobile.org/contacts/js/activities.js +++ b/assets/webapps/communications.gaiamobile.org/contacts/js/activities.js @@ -19,7 +19,7 @@ const VCARD_MIME_TYPES = [ var ActivityHandler = { _currentActivity: null, - _launchedAsInlineActivity: (window.location.search == '?pick'), + _launchedAsInlineActivity: new URLSearchParams(window.location.search).has('pick'), mozContactParam: null, @@ -124,6 +124,7 @@ var ActivityHandler = { this.launch_activity(activity, 'add-parameters'); break; case 'pick': + if (window.VulpesCompat) this._launchedAsInlineActivity = true; if (!this._launchedAsInlineActivity) { return; } @@ -356,15 +357,18 @@ var ActivityHandler = { postNewSuccess: function ah_postNewSuccess(contact) { this._currentActivity.postResult({ contact: contact }); this._currentActivity = null; + if (window.VulpesCompat) this._launchedAsInlineActivity = false; }, postPickSuccess: function ah_postPickSuccess(result) { this._currentActivity.postResult(result); this._currentActivity = null; + if (window.VulpesCompat) this._launchedAsInlineActivity = false; }, postCancel: function ah_postCancel() { this._currentActivity.postError('canceled'); this._currentActivity = null; + if (window.VulpesCompat) this._launchedAsInlineActivity = false; } }; diff --git a/assets/webapps/communications.gaiamobile.org/contacts/js/contacts.js b/assets/webapps/communications.gaiamobile.org/contacts/js/contacts.js index 64dc91a5..4ea36df1 100644 --- a/assets/webapps/communications.gaiamobile.org/contacts/js/contacts.js +++ b/assets/webapps/communications.gaiamobile.org/contacts/js/contacts.js @@ -655,7 +655,9 @@ var Contacts = (function() { utils.PerformanceHelper.contentInteractive(); utils.PerformanceHelper.chromeInteractive(); window.setTimeout(Contacts && Contacts.onLocalized); - if (window.navigator.mozSetMessageHandler && window.self == window.top) { + // Vulpes embeds each app in its own permission-checked frame. + if (window.navigator.mozSetMessageHandler && + (window.self == window.top || window.VulpesCompat)) { LazyLoader.load(['/shared/js/contacts/import/utilities/misc.js', '/shared/js/contacts/import/utilities/vcard_reader.js', '/shared/js/contacts/import/utilities/vcard_parser.js'], diff --git a/assets/webapps/communications.gaiamobile.org/contacts/js/views/form.js b/assets/webapps/communications.gaiamobile.org/contacts/js/views/form.js index 865eead5..ef5e805c 100644 --- a/assets/webapps/communications.gaiamobile.org/contacts/js/views/form.js +++ b/assets/webapps/communications.gaiamobile.org/contacts/js/views/form.js @@ -1038,6 +1038,8 @@ contacts.Form = (function() { // (Bug 1041455). if (editingContact) { Contacts.setCurrent(contact); + // The change event may arrive while the edit form is still visible. + Contacts.updateContactDetail(contact.id); } } ); diff --git a/assets/webapps/homescreen.gaiamobile.org/gaia_build_defer_index.js b/assets/webapps/homescreen.gaiamobile.org/gaia_build_defer_index.js index 20c50b10..6b936f7b 100644 --- a/assets/webapps/homescreen.gaiamobile.org/gaia_build_defer_index.js +++ b/assets/webapps/homescreen.gaiamobile.org/gaia_build_defer_index.js @@ -846,7 +846,7 @@ Apps.prototype={get iconSize(){if(!this._iconSize){var children=this.icons.child return this._iconSize;},toggleSmall:function(small){if(this.icons.classList.contains('small')===small){return;} this.icons.classList.toggle('small',small);this.icons.synchronise();this.refreshGridSize();this.snapScrollPosition();},toggleScrollSnapping:function(scrollSnapping){if(this.scrollable.classList.contains('snapping')===scrollSnapping){return;} this.scrollable.classList.toggle('snapping',scrollSnapping);this.snapScrollPosition();},onVisualLoad:function(){this.visualLoadComplete=true;this.icons.thaw();this.icons.classList.remove('loading');window.performance.mark('visuallyLoaded');window.performance.mark('contentInteractive');},addApp:function(app){var manifest=app.manifest||app.updateManifest;if(!manifest){return;} -if(BLACKLIST.includes(app.origin)){return;} +if(HIDDEN_ROLES.includes(manifest.role)||BLACKLIST.includes(app.origin)){return;} if(manifest.entry_points){for(var entryPoint in manifest.entry_points){this.addAppIcon(app,entryPoint);}}else{this.addAppIcon(app);}},addIconContainer:function(icon,entry){var container=document.createElement('div');container.classList.add('icon-container');container.order=-1;container.appendChild(icon);if(entry!==-1&&this.startupMetadata[entry].order>=0){container.order=this.startupMetadata[entry].order;var children=this.icons.children;for(var i=0,iLen=children.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 literal 0 HcmV?d00001 diff --git a/assets/webapps/marketplace.gaiamobile.org/index.html b/assets/webapps/marketplace.gaiamobile.org/index.html new file mode 100644 index 00000000..bee23bd1 --- /dev/null +++ b/assets/webapps/marketplace.gaiamobile.org/index.html @@ -0,0 +1,5 @@ +Marketplace + + + + diff --git a/assets/webapps/marketplace.gaiamobile.org/manifest.webapp b/assets/webapps/marketplace.gaiamobile.org/manifest.webapp new file mode 100644 index 00000000..8cded036 --- /dev/null +++ b/assets/webapps/marketplace.gaiamobile.org/manifest.webapp @@ -0,0 +1,19 @@ +{ + "name": "Marketplace", + "description": "Vulpes OS application catalogue", + "launch_path": "/index.html", + "type": "certified", + "icons": { + "512": "/icon.png" + }, + "permissions": { + "browser": {}, + "webapps-manage": {} + }, + "locales": { + "fr": { + "name": "Marketplace", + "description": "Catalogue des applications Vulpes OS" + } + } +} diff --git a/assets/webapps/marketplace.gaiamobile.org/marketplace.css b/assets/webapps/marketplace.gaiamobile.org/marketplace.css new file mode 100644 index 00000000..0125ce10 --- /dev/null +++ b/assets/webapps/marketplace.gaiamobile.org/marketplace.css @@ -0,0 +1 @@ +html,body{width:100%;height:100%;margin:0;background:#f4f4f4;font-family:Fira Sans,sans-serif}#catalogue{position:absolute;inset:0;width:100%;height:100%;border:0}#message{padding:3rem 2rem;text-align:center;font-size:1.8rem}#message button{display:block;width:100%;padding:1rem;margin:1rem 0;font:inherit}#open[hidden],[hidden]{display:none!important}#confirm{z-index:10}#confirm section{overflow:auto}#permissions{font-size:1.5rem} diff --git a/assets/webapps/marketplace.gaiamobile.org/marketplace.js b/assets/webapps/marketplace.gaiamobile.org/marketplace.js new file mode 100644 index 00000000..84dbc0c5 --- /dev/null +++ b/assets/webapps/marketplace.gaiamobile.org/marketplace.js @@ -0,0 +1,32 @@ +(async()=>{ + const frame=document.getElementById('catalogue'),message=document.getElementById('message'),dialog=document.getElementById('confirm'); + let attempt=0,busy=false,prepared=null,installed=null,language=await VulpesCompat.call('locale.current'); + const fr=()=>/^fr(?:-|$)/i.test(language); + const text=(f,e)=>fr()?f:e; + function labels(){document.getElementById('browse').textContent=text('Ouvrir le catalogue','Open catalogue');document.getElementById('open').textContent=text('Ouvrir','Open');document.getElementById('install').textContent=text('Installer','Install');document.getElementById('cancel').textContent=text('Annuler','Cancel');} + function loadFailed(){status(text('Catalogue inaccessible. Vérifiez la connexion Internet, puis réessayez.','Catalogue unavailable. Check your Internet connection, then try again.'));document.getElementById('open').hidden=true;} + frame.addEventListener('mozbrowsererror',loadFailed); + function browse(){labels();message.hidden=true;dialog.hidden=true;frame.style.visibility='visible';frame.src='https://vulpes-os.org/marketplace/?mode=app&lang='+(fr()?'fr':'en');} + function status(value){frame.style.visibility='hidden';message.hidden=false;document.getElementById('status').textContent=value;} + async function prepare(url){ + if(busy)return;const current=++attempt;busy=true;prepared=null;installed=null;document.getElementById('open').hidden=true; + status(text('Vérification de l’application…','Checking application…')); + try {const value=await VulpesCompat.call('marketplace.prepare',{url});if(current!==attempt)return;prepared=value; + document.getElementById('title').textContent=prepared.name; + document.getElementById('question').textContent=text('Installer cette application archivée ?','Install this archived application?')+' ('+(prepared.bytes/1048576).toFixed(1)+' Mio)'; + document.getElementById('permissions').textContent=text('Elle sera exécutée comme une application web, sans accès aux fonctions système. Certains anciens services peuvent ne plus fonctionner.','It will run as a web application without system access. Some old services may no longer work.'); + message.hidden=true;dialog.hidden=false; + }catch(error){if(current!==attempt)return;status(text('Installation impossible : ','Cannot install: ')+error.message);busy=false;} + } + dialog.addEventListener('submit',async event=>{event.preventDefault();if(!prepared)return;const token=prepared.token;prepared=null;dialog.hidden=true;status(text('Installation…','Installing…')); + try {installed=await VulpesCompat.call('marketplace.install',{token});status(text('Application ajoutée à l’accueil.','Application added to Home.'));document.getElementById('open').hidden=false;} + catch(error){status(text('Installation impossible : ','Cannot install: ')+error.message);}finally{busy=false;} + }); + const cancel=()=>{attempt++;prepared=null;busy=false;VulpesCompat.call('marketplace.cancel').catch(console.error);browse();}; + document.getElementById('cancel').onclick=cancel; + document.getElementById('browse').onclick=cancel; + document.getElementById('open').onclick=()=>installed && VulpesCompat.call('apps.launch',{manifestURL:installed.manifestURL}); + addEventListener('languagechange',()=>{language=navigator.language;if(!busy)browse();}); + browse(); + setInterval(async()=>{if(busy)return;try{const url=await VulpesCompat.call('marketplace.poll');if(url)await prepare(url);}catch(error){console.error(error);}},750); +})(); diff --git a/assets/webapps/ringtones.gaiamobile.org/gaia_build_defer_manage.js b/assets/webapps/ringtones.gaiamobile.org/gaia_build_defer_manage.js index ecc52c01..3ec99130 100644 --- a/assets/webapps/ringtones.gaiamobile.org/gaia_build_defer_manage.js +++ b/assets/webapps/ringtones.gaiamobile.org/gaia_build_defer_manage.js @@ -471,7 +471,7 @@ this.continue();}else{resolve(results);}};cursor.onerror=function(){if(this.erro return{remove:remove,get:get,list:list};})();;'use strict';function TonePlayer(){this._currentTone=null;this._isValid=true;this._player=new Audio();this._player.addEventListener('loadedmetadata',function(){if(this._player.src){this._isValid=true;this._player.dispatchEvent(new CustomEvent('validated',{detail:this._isValid}));}}.bind(this));this._player.addEventListener('error',function(){if(this._player.src){this._isValid=false;this._player.dispatchEvent(new CustomEvent('validated',{detail:this._isValid}));}}.bind(this));this._player.addEventListener('ended',function(){this._firePlayingCallback(false);}.bind(this));window.addEventListener('visibilitychange',function(){if(document.hidden){this.stop();this._setExclusiveMode(false);}}.bind(this));} TonePlayer.prototype={setTone:function(tone,callback){if(tone!==this._currentTone){this._firePlayingCallback(false);this._currentTone=tone;this._playingCallback=callback;if(tone&&tone.url){this._isValid=undefined;this._player.src=tone.url;this._player.play();this._firePlayingCallback(true);this._setExclusiveMode(true);}else{this._isValid=true;this._player.removeAttribute('src');this._player.load();}}else{if(!this._isValid){return;} if(this._player.paused||this._player.ended){this._player.currentTime=0;this._player.play();this._firePlayingCallback(true);this._setExclusiveMode(true);}else{this._player.pause();this._firePlayingCallback(false);}}},stop:function(){this._player.pause();this._firePlayingCallback(false);},get currentTone(){return this._currentTone;},isValid:function(callback){if(this._isValid!==undefined){callback(this._isValid);return;} -this._player.addEventListener('validated',function validated(event){this.removeEventListener('validated',validated);callback(event.detail);});},_firePlayingCallback:function(playing){if(this._playingCallback){this._playingCallback(playing);}},_setExclusiveMode:function(exclusive){if(exclusive){if(!this._source){this._context=new AudioContext('ringer');this._source=this._context.createMediaElementSource(this._player);this._source.connect(this._context.destination);}}else{if(this._source){this._source.disconnect();this._context=null;this._source=null;}}}};;'use strict';var ToneList=(function(){var NONE_ID='none:none';function toneCompare(a,b){if(a.id===NONE_ID&&b.id===NONE_ID){return 0;}else if(a.id===NONE_ID){return-1;}else if(b.id===NONE_ID){return 1;} +this._player.addEventListener('validated',function validated(event){this.removeEventListener('validated',validated);callback(event.detail);});},_firePlayingCallback:function(playing){if(this._playingCallback){this._playingCallback(playing);}},_setExclusiveMode:function(exclusive){if(exclusive){if(!this._source){this._context=new AudioContext();this._source=this._context.createMediaElementSource(this._player);this._source.connect(this._context.destination);}}else{if(this._source){this._source.disconnect();this._context=null;this._source=null;}}}};;'use strict';var ToneList=(function(){var NONE_ID='none:none';function toneCompare(a,b){if(a.id===NONE_ID&&b.id===NONE_ID){return 0;}else if(a.id===NONE_ID){return-1;}else if(b.id===NONE_ID){return 1;} var aName=a.name.toLocaleLowerCase();var bName=b.name.toLocaleLowerCase();var cmp=aName.localeCompare(bName);if(cmp){return cmp;} var aSubtitle=(a.subtitle||'').toLocaleLowerCase();var bSubtitle=(b.subtitle||'').toLocaleLowerCase();return aSubtitle.localeCompare(bSubtitle);} function domify(htmlText){var dummyDiv=document.createElement('div');dummyDiv.innerHTML=htmlText;var element=dummyDiv.firstElementChild;return element;} diff --git a/assets/webapps/ringtones.gaiamobile.org/gaia_build_defer_pick.js b/assets/webapps/ringtones.gaiamobile.org/gaia_build_defer_pick.js index 22920cc3..504d9d47 100644 --- a/assets/webapps/ringtones.gaiamobile.org/gaia_build_defer_pick.js +++ b/assets/webapps/ringtones.gaiamobile.org/gaia_build_defer_pick.js @@ -447,7 +447,7 @@ this.continue();}else{resolve(results);}};cursor.onerror=function(){if(this.erro return{remove:remove,get:get,list:list};})();;;'use strict';function TonePlayer(){this._currentTone=null;this._isValid=true;this._player=new Audio();this._player.addEventListener('loadedmetadata',function(){if(this._player.src){this._isValid=true;this._player.dispatchEvent(new CustomEvent('validated',{detail:this._isValid}));}}.bind(this));this._player.addEventListener('error',function(){if(this._player.src){this._isValid=false;this._player.dispatchEvent(new CustomEvent('validated',{detail:this._isValid}));}}.bind(this));this._player.addEventListener('ended',function(){this._firePlayingCallback(false);}.bind(this));window.addEventListener('visibilitychange',function(){if(document.hidden){this.stop();this._setExclusiveMode(false);}}.bind(this));} TonePlayer.prototype={setTone:function(tone,callback){if(tone!==this._currentTone){this._firePlayingCallback(false);this._currentTone=tone;this._playingCallback=callback;if(tone&&tone.url){this._isValid=undefined;this._player.src=tone.url;this._player.play();this._firePlayingCallback(true);this._setExclusiveMode(true);}else{this._isValid=true;this._player.removeAttribute('src');this._player.load();}}else{if(!this._isValid){return;} if(this._player.paused||this._player.ended){this._player.currentTime=0;this._player.play();this._firePlayingCallback(true);this._setExclusiveMode(true);}else{this._player.pause();this._firePlayingCallback(false);}}},stop:function(){this._player.pause();this._firePlayingCallback(false);},get currentTone(){return this._currentTone;},isValid:function(callback){if(this._isValid!==undefined){callback(this._isValid);return;} -this._player.addEventListener('validated',function validated(event){this.removeEventListener('validated',validated);callback(event.detail);});},_firePlayingCallback:function(playing){if(this._playingCallback){this._playingCallback(playing);}},_setExclusiveMode:function(exclusive){if(exclusive){if(!this._source){this._context=new AudioContext('ringer');this._source=this._context.createMediaElementSource(this._player);this._source.connect(this._context.destination);}}else{if(this._source){this._source.disconnect();this._context=null;this._source=null;}}}};;;'use strict';var ToneList=(function(){var NONE_ID='none:none';function toneCompare(a,b){if(a.id===NONE_ID&&b.id===NONE_ID){return 0;}else if(a.id===NONE_ID){return-1;}else if(b.id===NONE_ID){return 1;} +this._player.addEventListener('validated',function validated(event){this.removeEventListener('validated',validated);callback(event.detail);});},_firePlayingCallback:function(playing){if(this._playingCallback){this._playingCallback(playing);}},_setExclusiveMode:function(exclusive){if(exclusive){if(!this._source){this._context=new AudioContext();this._source=this._context.createMediaElementSource(this._player);this._source.connect(this._context.destination);}}else{if(this._source){this._source.disconnect();this._context=null;this._source=null;}}}};;;'use strict';var ToneList=(function(){var NONE_ID='none:none';function toneCompare(a,b){if(a.id===NONE_ID&&b.id===NONE_ID){return 0;}else if(a.id===NONE_ID){return-1;}else if(b.id===NONE_ID){return 1;} var aName=a.name.toLocaleLowerCase();var bName=b.name.toLocaleLowerCase();var cmp=aName.localeCompare(bName);if(cmp){return cmp;} var aSubtitle=(a.subtitle||'').toLocaleLowerCase();var bSubtitle=(b.subtitle||'').toLocaleLowerCase();return aSubtitle.localeCompare(bSubtitle);} function domify(htmlText){var dummyDiv=document.createElement('div');dummyDiv.innerHTML=htmlText;var element=dummyDiv.firstElementChild;return element;} diff --git a/assets/webapps/ringtones.gaiamobile.org/js/tone_player.js b/assets/webapps/ringtones.gaiamobile.org/js/tone_player.js index 18cf4537..dfb17d68 100644 --- a/assets/webapps/ringtones.gaiamobile.org/js/tone_player.js +++ b/assets/webapps/ringtones.gaiamobile.org/js/tone_player.js @@ -1,4 +1,4 @@ ;;'use strict';function TonePlayer(){this._currentTone=null;this._isValid=true;this._player=new Audio();this._player.addEventListener('loadedmetadata',function(){if(this._player.src){this._isValid=true;this._player.dispatchEvent(new CustomEvent('validated',{detail:this._isValid}));}}.bind(this));this._player.addEventListener('error',function(){if(this._player.src){this._isValid=false;this._player.dispatchEvent(new CustomEvent('validated',{detail:this._isValid}));}}.bind(this));this._player.addEventListener('ended',function(){this._firePlayingCallback(false);}.bind(this));window.addEventListener('visibilitychange',function(){if(document.hidden){this.stop();this._setExclusiveMode(false);}}.bind(this));} TonePlayer.prototype={setTone:function(tone,callback){if(tone!==this._currentTone){this._firePlayingCallback(false);this._currentTone=tone;this._playingCallback=callback;if(tone&&tone.url){this._isValid=undefined;this._player.src=tone.url;this._player.play();this._firePlayingCallback(true);this._setExclusiveMode(true);}else{this._isValid=true;this._player.removeAttribute('src');this._player.load();}}else{if(!this._isValid){return;} if(this._player.paused||this._player.ended){this._player.currentTime=0;this._player.play();this._firePlayingCallback(true);this._setExclusiveMode(true);}else{this._player.pause();this._firePlayingCallback(false);}}},stop:function(){this._player.pause();this._firePlayingCallback(false);},get currentTone(){return this._currentTone;},isValid:function(callback){if(this._isValid!==undefined){callback(this._isValid);return;} -this._player.addEventListener('validated',function validated(event){this.removeEventListener('validated',validated);callback(event.detail);});},_firePlayingCallback:function(playing){if(this._playingCallback){this._playingCallback(playing);}},_setExclusiveMode:function(exclusive){if(exclusive){if(!this._source){this._context=new AudioContext('ringer');this._source=this._context.createMediaElementSource(this._player);this._source.connect(this._context.destination);}}else{if(this._source){this._source.disconnect();this._context=null;this._source=null;}}}}; \ No newline at end of file +this._player.addEventListener('validated',function validated(event){this.removeEventListener('validated',validated);callback(event.detail);});},_firePlayingCallback:function(playing){if(this._playingCallback){this._playingCallback(playing);}},_setExclusiveMode:function(exclusive){if(exclusive){if(!this._source){this._context=new AudioContext();this._source=this._context.createMediaElementSource(this._player);this._source.connect(this._context.destination);}}else{if(this._source){this._source.disconnect();this._context=null;this._source=null;}}}}; \ No newline at end of file diff --git a/changelog.json b/changelog.json index 81936731..4efc193f 100644 --- a/changelog.json +++ b/changelog.json @@ -5,6 +5,69 @@ "en": "# Changelog — Vulpes OS\n\n[Français](CHANGELOG.md)\n\nOne Gaia base, three platforms: **desktop**, **Android (APK)** and **Tundra / Sargo**.\nThis journal covers visible changes and major technical milestones.\n\n**Status:** “In development” refers to local sources, “APK built” to a test package,\nand “Published” to a public repository release. Shared source changes reach installed\nAPKs and phone images only after those packages are updated." }, "entries": [ + { + "id": "2026-10-08-source-publication", + "text": { + "fr": "## 8 octobre 2026 — Mise à jour des sources publiques\n\n### Commun · PC / Android / Tundra\n\n- Regrouper les correctifs récents de caméra, Contacts, SMS, Marketplace et les outils de construction et de restauration Sargo dans les sources publiques. Mettre à jour les README français et anglais avec les capacités et limites vérifiées.\n- Corriger le contrôle de parité Android pour créer son dossier de rapports sur une copie neuve du dépôt. Vérifier que la configuration Wi-Fi native embarque un répertoire de connexions vide.\n- Conserver les tickets, feuilles de route, documents internes, données personnelles et clés hors du dépôt.\n\n### Paquets et qualification\n\n- L’APK public reste Preview17 : les correctifs du 8 octobre nécessitent une prochaine compilation. Aucune nouvelle image native publiée ni flash dans cette étape.\n- Sur Sargo, l’enregistrement cellulaire reste en recherche lors du dernier contrôle ; audio bidirectionnel, appairage Bluetooth et profils restent à qualifier.", + "en": "## 8 October 2026 — Public source update\n\n### Shared · Desktop / Android / Tundra\n\n- Bring recent camera, Contacts, SMS and Marketplace fixes, plus Sargo build and recovery tools, into the public sources. Update both READMEs with verified capabilities and limitations.\n- Make the Android parity check create its report directory in a fresh checkout. Verify that native Wi-Fi configuration ships an empty connections directory.\n- Keep tickets, roadmaps, internal documents, personal data and keys outside the repository.\n\n### Packages and qualification\n\n- The public APK remains Preview17; the 8 October fixes require a future build. No new native image or flash in this step.\n- At the latest Sargo check, cellular registration is still searching; two-way call audio, Bluetooth pairing and profiles remain to be qualified." + } + }, + { + "id": "development-2026-10-08-camera-lifecycle", + "text": { + "fr": "## 8 octobre 2026 — Caméra et parcours Contacts · en développement\n\n### Commun · Gaia / PC / Android / Tundra\n\n- Sérialiser les ouvertures de caméra, attendre la fermeture du capteur précédent et libérer les pistes et ressources vidéo même après un échec. Stabiliser l’identification avant/arrière sur PC et Android.\n- Conserver les images binaires lors des échanges PC/Tundra ; encoder explicitement les photos et dates des contacts pour le pont JSON Android.\n- Corriger l’actualisation de la fiche après modification d’un contact. Rétablir les activités Contacts → SMS, sélection d’un numéro et ouverture du pavé numérique, en préservant les demandes imbriquées et les points d’entrée de Gaia.\n- Réouvrir Galerie en mode sélection pour ajouter une photo à un contact, même lorsque Galerie est déjà ouverte en consultation. Sélection, recadrage et enregistrement de la photo validés sur PC.\n- Ajouter des tests de caméra, de stockage des contacts, de permissions, de transport binaire et de routage des activités.\n\n### PC\n\n- Vérifier le parcours original Appareil photo → capture JPEG → Galerie → ouverture de la photo → retour caméra.\n- Vérifier dans Gaia : création et modification d’un contact, actualisation de la fiche, recherche sans accents et sans résultats, favoris, destinataire SMS, sélection d’un numéro, pavé numérique prérempli, annulation puis confirmation de suppression. Le test n’émet ni appel ni SMS et supprime son contact temporaire.\n\n### Tundra · Sargo\n\n- Vérifier en RAM sur Pixel 3a : autofocus, zoom, quatre alternances avant/arrière avec nouvelles trames, capture JPEG depuis Gaia, stockage, ouverture dans Galerie et arrêt du capteur à l’accueil.\n- Vérifier les conversations déjà stockées, le clavier et les notifications. Recherche Wi-Fi : quatre réseaux détectés. Nouvelle émission/réception SMS et audio des appels encore en qualification.\n- Ajouter les paquets BlueZ et les bibliothèques nécessaires, verrouillés par SHA256. Le contrôleur Bluetooth démarre, s’active et lance une recherche ; l’appairage depuis Gaia et les profils ne sont pas encore validés.\n- Ajouter le magasin de certificats natif : HTTPS vérifié sans désactivation de TLS. Tester les archives altérées, collisions de fichiers et reconstructions répétées.\n\n- Corriger les fichiers Contacts embarqués dans le build de démarrage : la création, l’édition, le passage vers SMS, le sélecteur et le pavé numérique passent aussi avec les gestes tactiles sur Sargo.\n\n### Android · APK\n\n- Publier l’APK Preview17 existant, avec téléchargement et SHA256 vérifiés. Il précède les correctifs du 8 octobre.\n- Corriger la transmission des destinataires et du texte de Gaia vers l’application SMS Android, y compris la réponse à une conversation et le numéro saisi sans validation préalable. Tests du pont réussis ; essai Android réel encore nécessaire.\n- Réempaqueter les ressources communes corrigées pour la prochaine compilation ; aucun nouvel APK incluant ces correctifs n’est publié.\n\n**État :** builds natifs de qualification démarrés en RAM. Aucun flash, push de code ni publication de documentation privée dans cette étape. La qualification complète des fonctions de téléphone reste ouverte.\n", + "en": "## 8 October 2026 — Camera and Contacts workflows · in development\n\n### Shared · Gaia / Desktop / Android / Tundra\n\n- Serialize camera opening, await the previous sensor shutdown and release tracks and video resources after failures. Keep stable front/rear identities on desktop and Android.\n- Preserve binary images across desktop/Tundra transport; explicitly encode contact photos and dates for Android's JSON bridge.\n- Refresh contact details after editing. Restore Contacts → SMS, number picking and prefilled dialer activities, preserving nested requests and Gaia entry points.\n- Reopen Gallery in picker mode when adding a contact photo, even when Gallery is already open for browsing. Selection, cropping and photo persistence verified on desktop.\n- Add camera lifecycle, contact persistence, permissions, binary transport and activity routing tests.\n\n### Desktop\n\n- Verify the original Camera → JPEG capture → Gallery → photo opening → camera workflow.\n- Verify Gaia contact creation, editing, detail refresh, accent-insensitive search and empty results, favourites, SMS recipient, number picker, prefilled keypad, cancelled deletion and confirmed deletion. The test sends no message and makes no call; its temporary contact is removed.\n\n### Tundra · Sargo\n\n- Verify on a RAM-booted Pixel 3a: autofocus, zoom, four front/rear switches with fresh frames, shutter capture, JPEG persistence, Gallery opening and sensor shutdown on Home.\n- Check stored conversations, keyboard and notifications. Wi-Fi scan finds four networks. New SMS transmission/reception and call audio are still being qualified.\n- Add SHA256-pinned BlueZ packages and required libraries. The controller starts, powers on and discovers; pairing through Gaia and Bluetooth profiles are not yet validated.\n- Add native trusted certificates and verify HTTPS without disabling TLS. Test corrupted archives, file collisions and repeatable staging.\n\n- Include the corrected Contacts files in the boot build: contact creation, editing, SMS handoff, picking and dialer prefilling also pass using touch gestures on Sargo.\n\n### Android · APK\n\n- Publish the existing Preview17 APK and verify its download and SHA256. It predates the 8 October fixes.\n- Fix recipient and body handoff from Gaia to the Android SMS app, including replies and a typed recipient that was not yet committed. Bridge tests pass; real Android validation is still required.\n- Repackage corrected shared resources for the next build; no APK containing these new fixes has been published.\n\n**Status:** native qualification builds booted in RAM. No flashing, source push or private documentation publication in this step. Full phone qualification remains open.\n" + } + }, + { + "id": "development-2026-10-06-marketplace-theme", + "text": { + "fr": "## 6 octobre 2026 — Thème Marketplace intégré · en développement\n\n### Commun · Gaia / PC / Android / Tundra\n\n- Ouvrir le catalogue avec `mode=app`, conservé dans les liens, la recherche, les filtres, les fiches et le changement de langue.\n- Préparer un thème web Vulpes dédié : Fira locale, navigation compacte, boutons tactiles, cartes lisibles, filtres repliables et captures défilantes. Retirer les en-têtes du site dans le mode intégré, tout en conservant l’interface du site classique.\n- Conserver les confirmations et messages système côté Gaia. Le thème ne donne aucun privilège supplémentaire aux pages du catalogue.\n- Vérifier 16 pages FR/EN, 2 pages en mode classique et les ressources du thème. Rendus mobiles de 320 et 360 pixels inspectés. Lanceurs PC et Sargo mis à jour ; les sources seront reprises au prochain empaquetage Android.\n\nFichiers du site prêts localement, non déployés sur le serveur public. Aucun push ni nouvelle publication APK/ROM.\n", + "en": "## 6 October 2026 — Embedded Marketplace theme · in development\n\n### Shared · Gaia / Desktop / Android / Tundra\n\n- Open the catalogue with `mode=app`, retained in navigation, search, filters, app details and language switches.\n- Prepare a dedicated Vulpes web theme with local Fira fonts, compact navigation, touch-friendly controls, readable cards, collapsible filters and scrolling screenshots. Omit website headers in embedded mode while preserving the standard website layout.\n- Keep confirmations and system messages in Gaia. The theme grants catalogue pages no additional privileges.\n- Check 16 French/English pages, 2 standard pages and theme assets. Inspect mobile renders at 320 and 360 pixels. Desktop and Sargo launchers updated; shared sources will be included in the next Android packaging run.\n\nWebsite files prepared locally, not deployed to the public server. No push or new APK/ROM publication.\n" + } + }, + { + "id": "development-2026-10-06-system-banner", + "text": { + "fr": "## 6 octobre 2026 — Bandeaux Gaia · en développement\n\n### Commun · Gaia / PC / Android / Tundra\n\n- Faire disparaître le bandeau « Application installée » après son délai normal, même si la fin d’animation ne remonte pas du composant Gaia. Conserver son apparence et permettre sa fermeture au toucher.\n- Annuler les anciens délais lorsqu’un nouveau message remplace le précédent pour éviter une disparition prématurée.\n- Vérifier sur PC l’affichage, l’expiration, le remplacement et la fermeture au clic ; vérifier sur le Pixel le retrait effectif du bandeau après cinq secondes. Correctif déployé sur l’essai Sargo en RAM et disponible dans les sources communes pour le prochain APK.\n\nAucun flash ni publication dans cette étape.\n", + "en": "## 6 October 2026 — Gaia banners · in development\n\n### Shared · Gaia / Desktop / Android / Tundra\n\n- Dismiss the “Application installed” banner after its normal timeout even if the animation-end event does not reach the Gaia component host. Keep its appearance and tap-to-dismiss behavior.\n- Cancel old timers when a new message replaces the previous one, preventing premature dismissal.\n- Check display, expiry, replacement and click dismissal on desktop; verify actual removal after five seconds on the Pixel. Fix deployed to the Sargo RAM trial and available in shared sources for the next APK.\n\nNo flash or publication in this step.\n" + } + }, + { + "id": "development-2026-10-06-wifi", + "text": { + "fr": "## 6 octobre 2026 — Wi-Fi et panneaux Paramètres · en développement\n\n### Commun · Gaia / PC / Android / Tundra\n\n- Terminer l’ouverture d’un panneau Paramètres même si Gecko ne produit pas l’événement de fin d’animation. Conserver les transitions Gaia et éviter les doubles initialisations.\n- Correctif dans les sources communes et le serveur PC ; inclus au prochain empaquetage Android. Aucun nouvel APK publié dans cette étape.\n\n### Tundra · Sargo\n\n- Corriger les événements de signal Wi-Fi : transmettre le réseau associé et ne pas annoncer de signal de connexion lorsque le téléphone est déconnecté. L’événement incomplet faisait échouer l’affichage des réseaux dans Gaia.\n- Sur le Pixel, vérifier le pilote, le service natif, le passage par Gecko et l’affichage du panneau corrigé : quatre points d’accès détectés, regroupés en deux réseaux, sans erreur de recherche.\n- Connexion Wi-Fi confirmée par l’utilisateur ; requête HTTPS réelle réussie depuis Gecko vers example.org (HTTP 200, vérification des certificats active). Correctifs appliqués à l’essai en RAM ; aucun flash ni push.\n", + "en": "## 6 October 2026 — Wi-Fi and Settings panels · in development\n\n### Shared · Gaia / Desktop / Android / Tundra\n\n- Complete Settings panel navigation even when Gecko does not emit an animation-end event. Preserve Gaia transitions and prevent duplicate initialization.\n- Updated shared sources and the desktop server; included in the next Android packaging run. No new APK published in this step.\n\n### Tundra · Sargo\n\n- Fix Wi-Fi signal events: include the associated network and do not emit connection information while disconnected. The incomplete event prevented Gaia from displaying scan results.\n- Check the driver, native service, Gecko bridge and corrected panel on the Pixel: four access points detected, grouped into two networks, with no scan error.\n- Wi-Fi connection confirmed by the user; a real HTTPS request from Gecko to example.org passed (HTTP 200, certificate verification enabled). Fixes applied to the RAM trial; no flash or Git push.\n" + } + }, + { + "id": "development-2026-10-06-marketplace-views", + "text": { + "fr": "## 6 octobre 2026 — Marketplace · en développement\n\n### PC / Tundra · Sargo\n\n- Maintenir le catalogue sous la barre de statut et dans les limites de son application. Masquer sa vue web à l’accueil et dans le multitâche ; la retirer à la fermeture de l’application.\n- Éviter qu’une navigation annulée ou l’ancienne page d’erreur masque une nouvelle tentative.\n- Afficher un message local et proposer une nouvelle tentative lorsque le catalogue est inaccessible, au lieu de laisser sa page d’erreur recouvrir Gaia.\n- Sur PC : navigation, erreur réseau, nouvelle tentative, retour à l’accueil et fermeture depuis le multitâche vérifiés. Sept contrôles directs passent sur Sargo en RAM, sans fenêtre de débogage, avec une page locale et une précondition de déverrouillage forcée ; le catalogue distant nécessite une connexion Internet sur le téléphone.\n\n### Android · APK\n\n- Reconstruire l’APK local Preview17 avec le message d’erreur Marketplace commun. Signature, alignement et parité des ressources vérifiés ; nouvelle régression sur appareil Android non effectuée dans cette étape.\n\nAucune publication dans cette étape.\n", + "en": "## 6 October 2026 — Marketplace · in development\n\n### Desktop / Tundra · Sargo\n\n- Keep the catalogue below the status bar and inside its owning application. Hide its web view on Home and in the task switcher; remove it when the app closes.\n- Prevent a cancelled navigation or a previous error page from hiding a retry.\n- Show a local error message with a retry action when the catalogue is unavailable, instead of leaving its error page over Gaia.\n- Desktop navigation, network failure, retry, Home and closing from the task switcher checked. Seven direct checks pass on Sargo in RAM without a debugger window, using a local page and a forced-unlock precondition; the remote catalogue requires an Internet connection on the phone.\n\n### Android · APK\n\n- Rebuild the local Preview17 APK with the shared Marketplace error message. Signature, alignment and asset parity checked; no new Android device regression run in this step.\n\nNothing published in this step.\n" + } + }, + { + "id": "2.7-preview.17", + "text": { + "fr": "## 2.7-preview.17 — APK construit · 5 octobre 2026\n\n### Commun · Gaia / PC\n\n- Remettre Marketplace sur l’accueil avec son icône originale et le catalogue Vulpes. Ouvrir sa version française pour les langues fr, anglaise sinon.\n- Adapter les téléchargements du catalogue en installations confirmées : vérifier les archives ZIP, ajouter l’application à l’accueil et permettre son lancement et sa désinstallation.\n- Conserver les applications installées séparément des sources Gaia, sur des origines distinctes et sans privilèges système hérités des anciens manifestes.\n- Valider sur PC le parcours réel avec 2048 : catalogue, confirmation, installation, lancement, persistance et désinstallation. Contrôler les archives corrompues, chemins invalides, limites, annulation et confirmations à usage unique.\n\n### Android · APK\n\n- Construire et signer Preview17 avec le même Marketplace et le même service d’installation. Utiliser la vue GeckoView existante pour le catalogue et le stockage privé Android pour les paquets.\n- Vérifier sur émulateur Android 16/API36 le clic Installer, la confirmation, l’apparition sur l’accueil et le lancement de 2048 sans accès au pont système Vulpes. Vérifier aussi la conservation après arrêt forcé et relance, puis la désinstallation.\n\n### Tundra · Sargo\n\n- Préparer une image de boot locale incluant Marketplace, l’installateur et le serveur des applications ajoutées. Démarrage en RAM effectué sur le Pixel le 6 octobre : accueil et ressources Marketplace vérifiés, sans flash. Neuf contrôles système sur dix passent ; le contrôle global Gaia relève une variation du viewport au déverrouillage. Installation de paquets sur Sargo encore à qualifier.\n\n**Limites :** compatibilité initiale avec les applications web empaquetées. Les anciennes API privilégiées, applications hébergées et services disparus ne sont pas restaurés automatiquement. Rien publié sur Forgejo ni sur le site.\n", + "en": "## 2.7-preview.17 — APK built · 5 October 2026\n\n### Shared · Gaia / Desktop\n\n- Restore Marketplace on Home with its original icon and the Vulpes catalogue. Select French for fr languages and English otherwise.\n- Turn catalogue downloads into confirmed installations: validate ZIP archives, add the application to Home and support launch and removal.\n- Store installed applications separately from Gaia sources, on distinct origins and without system privileges inherited from archived manifests.\n- Validate the real desktop workflow with 2048: catalogue, confirmation, installation, launch, persistence and removal. Check damaged archives, invalid paths, limits, cancellation and single-use confirmations.\n\n### Android · APK\n\n- Build and sign Preview17 with the same Marketplace and installation service. Use the existing GeckoView browser for the catalogue and private Android storage for packages.\n- Verify the Install click, confirmation, Home icon and 2048 launch without access to the Vulpes system bridge on an Android 16/API36 emulator. Also verify persistence after force-stop/relaunch and removal.\n\n### Tundra · Sargo\n\n- Prepare a local boot image containing Marketplace, the installer and the added-application server. RAM boot performed on the Pixel on 6 October: Home and Marketplace resources verified, without flashing. Nine of ten system checks pass; the overall Gaia check records a viewport change during unlocking. Package installation on Sargo still needs qualification.\n\n**Limitations:** initial support for packaged web applications. Old privileged APIs, hosted apps and discontinued services are not restored automatically. Nothing published to Forgejo or the website.\n" + } + }, + { + "id": "2.7-preview.16", + "text": { + "fr": "## 2.7-preview.16 — APK construit · 5 octobre 2026\n\n### Commun · Gaia / PC\n\n- Rétablir les sélecteurs Gaia de fond d’écran et de sons, leur retour vers Paramètres et leur annulation par Accueil. Partager le transport des images et sons enregistrés entre PC et Android.\n- Propager les changements de langue à l’accueil, y compris sans permission de lecture générale des réglages. Vérifier les libellés FR/EN et la persistance.\n- Rétablir l’arrêt du balayage horizontal sur une page et masquer les applications de développement sur l’accueil, sans les supprimer des sources.\n- Utiliser le logo Vulpes fourni dans le splash et adapter les composants originaux de la liste des sons au moteur actuel.\n- Vérifier sur PC deux changements consécutifs de fond, le choix d’un son et les panneaux Gaia restaurés.\n\n### Android · APK\n\n- APK local Preview16 construit et signé ; contenu commun et version vérifiés. Aucune publication dans cette étape.\n- Valider Vulpes comme lanceur HOME, afficher le verrouillage Gaia après veille et rétablir le retour Android depuis la caméra ouverte au verrouillage.\n- Masquer les barres système Android en mode immersif, en conservant leur accès temporaire par balayage.\n- Tests sur émulateur Android 16/API36 : verrouillage, caméra/retour, FR/EN, accueil sans applications de test, fond d’écran et son. Fond conservé après arrêt forcé et relance, sans changement répété pendant 15 secondes. Autres appareils Android non qualifiés dans cette étape.\n\n### Tundra · Sargo\n\n- Inclure les mêmes correctifs et la politique des applications dans une nouvelle image de boot de test. Le boot précédent réappliquait une copie ancienne des fichiers et annulait les modifications à chaud.\n- Démarrage en RAM sur le Pixel réussi : six contrôles de langues/accueil/sélecteurs et dix contrôles système passent. Sélecteurs image/son vérifiés sans écraser les choix personnels. Le boot installé reste Preview15 ; aucune partition flashée dans cette étape.\n\n**Limites :** l’installation tierce FFAA reste ouverte. Le site dépend d’anciennes API mozApps et d’un script nomodule ; un installateur isolant les applications et leurs permissions reste à construire. Le choix d’un son Gaia ne modifie pas la sonnerie globale Android ; l’audio d’appel réel n’a pas été qualifié ici.", + "en": "## 2.7-preview.16 — APK built · 5 October 2026\n\n### Shared · Gaia / Desktop\n\n- Restore Gaia wallpaper and sound pickers, return navigation to Settings and cancellation through Home. Share persisted image and sound transport between desktop and Android.\n- Propagate language changes to Home without granting general settings read access. Check French/English labels and persistence.\n- Restore horizontal page snapping and hide development apps from Home without deleting their sources.\n- Use the supplied Vulpes logo in the splash and adapt the original sound-list components to the current engine.\n- Verify two consecutive wallpaper changes, sound selection and restored Gaia panels on desktop.\n\n### Android · APK\n\n- Build and sign local Preview16; verify shared payload and version. Nothing published in this step.\n- Validate Vulpes as the HOME launcher, restore Gaia locking after sleep and Android Back navigation from the lockscreen camera.\n- Hide Android system bars in immersive mode while retaining temporary swipe access.\n- Android 16/API36 emulator checks: locking, camera/Back, French/English, Home without test apps, wallpaper and sound. Wallpaper retained after force-stop/relaunch with no repeated changes over 15 seconds. Other Android devices not qualified in this step.\n\n### Tundra · Sargo\n\n- Include shared fixes and app policy in a new test boot image. The previous boot reapplied an old file snapshot, undoing hot updates.\n- RAM boot on Pixel passed: six language/Home/picker checks and ten system checks. Image/sound picker results verified without replacing personal selections. Installed boot remains Preview15; no partition flashed in this step.\n\n**Limitations:** third-party FFAA installation remains open. The site depends on old mozApps APIs and a nomodule script; an installer with application and permission isolation still needs implementation. Selecting a Gaia sound does not change the global Android ringtone; actual call audio was not qualified here." + } + }, + { + "id": "2026-10-04-sargo-standalone", + "text": { + "fr": "## En développement — 4 octobre 2026 — Installation autonome Sargo\n\n### Tundra · Sargo\n\n- Construction d’un volume ext4 neuf directement destiné à userdata : système, données et composants matériels vendor/modem embarqués. Le démarrage ne cherche plus de fichiers dans une installation Droidian ; aucune session Android ni connexion ADB n’est nécessaire à l’installation.\n- Installateur Fastboot en Python : contrôle du modèle Sargo, du déverrouillage, des tailles et des empreintes, puis remplacement de userdata, dtbo_a, vbmeta_a et boot_a. Le bootloader reste déverrouillé ; les calibrations propres au téléphone sont conservées.\n- Sauvegarde complète de userdata hors ligne, comparaison intégrale avec le téléphone et vérification de l’archive de restauration avant le flash. Les sauvegardes restent privées ; une restauration réelle n’a pas été effectuée dans cette étape.\n- Deux démarrages depuis le stockage interne réussis : onze contrôles Gaia et dix contrôles natifs par démarrage, image de boot relue et vérifiée, réglages conservés. Le slot A est marqué réussi uniquement après validation du démarrage Gaia courant.\n- Variante générique sans serveur SSH ni clés de maintenance : flash de boot_a et démarrage réel réussis. La variante de développement est ensuite remise sur le Pixel pour poursuivre les travaux. Les images du paquet générique conservent leur état initial vierge.\n- Contrôles finaux sur le démarrage autonome de développement : Photo (dont bascules de capteur), clavier Gaia et notifications réussis.\n- Paquet Fastboot local assemblé avec empreintes et rapport de qualification. 44 tests ciblés passent. Il n’est pas encore publié : la provenance et la redistribution de certains composants matériels restent à finaliser.\n- Limites : qualification sur un Pixel 3a, pas sur toutes les combinaisons de firmware ; volume de 8 Gio comprenant 1 Gio de données modifiables, sans extension automatique. Les fonctionnalités matérielles restent expérimentales.\n\n### PC / Android · APK\n\n- Aucun changement d’interface ni nouveau binaire dans cette étape. Gaia reste partagé entre les trois plateformes.", + "en": "## In development — 4 October 2026 — Standalone Sargo installation\n\n### Tundra · Sargo\n\n- Build a fresh ext4 volume directly for userdata, including the system, writable data and vendor/modem hardware images. Startup no longer looks for files in a Droidian installation; installation requires neither an Android session nor ADB.\n- Python Fastboot installer checks the Sargo model, unlock state, partition sizes and checksums, then replaces userdata, dtbo_a, vbmeta_a and boot_a. The bootloader stays unlocked and device-specific calibration is retained.\n- Full offline userdata backup, complete comparison against the phone and recovery archive verification before flashing. Backups remain private; an actual restoration was not performed in this step.\n- Two successful boots from internal storage: eleven Gaia and ten native checks per boot, verified boot-image readback and retained settings. Slot A is marked successful only after validating Gaia startup in the current boot.\n- Generic variant without an SSH server or maintenance keys: actual boot_a flash and boot passed. The development variant was then restored on the Pixel for further work. Generic package images retain their original fresh state.\n- Final checks on the standalone development boot passed for Camera (including lens switches), the Gaia keyboard and notifications.\n- Local Fastboot package assembled with checksums and a qualification report. 44 targeted tests pass. It is not published yet: provenance and redistribution of some hardware components still need final review.\n- Limits: qualified on one Pixel 3a, not every firmware combination; an 8 GiB volume including 1 GiB of writable data, without automatic expansion. Hardware functionality remains experimental.\n\n### Desktop / Android · APK\n\n- No interface changes or new binary in this step. Gaia remains shared by all three platforms." + } + }, { "id": "2026-10-03-sargo-per-device-installation", "text": { @@ -185,6 +248,12 @@ "version": "2.7-preview.15", "after": "2.7-preview.14", "summary": "## 🇫🇷 Vulpes OS — Preview 15\n\nPremière publication des **sources communes PC / Android / Tundra**, accompagnée d'un nouvel APK expérimental.\n\n### Télécharger et essayer\n- **Android :** installer `vulpes-os-preview-2.7-preview.15.apk`. Android 8 minimum déclaré, API 37 ciblée ; ARM64, ARMv7 et x86_64. L'APK garde la signature des précédentes Preview.\n- **PC Linux :** cloner le dépôt, puis exécuter `./setup.sh` et `./run.sh`. Le moteur Mozilla épinglé est téléchargé et son empreinte vérifiée.\n- **Sargo :** les sources Tundra sont incluses. **Aucune ROM générique flashable n'est fournie** : notre image privée dépend encore de l'installation et du partitionnement du téléphone de développement.\n\n### Depuis la Preview 14\n- Correction du format des dates des SMS qui laissait les conversations Gaia vides ; conservation des messages et des états d'envoi/échec côté hôte.\n- Libération et remplacement des flux caméra fiabilisés, pour empêcher une ancienne session de fermer le nouveau capteur.\n- Sources Tundra : focus tactile, zoom numérique et capture JPEG du capteur ; aperçu distinct de la photo en pleine résolution. Coupure du vibreur corrigée et arrêt de secours du service.\n- Sources PC, APK et Tundra réunies, scripts de préparation et de compilation inclus, README français et anglais remis à jour. Les profils privés et la documentation de travail ne sont pas publiés.\n\n**Validation :** 42 tests de services, démarrage et navigation sur PC avec Gecko 156.0.1 ; sur émulateur Android 16, capture Photo/Galerie, reprise de caméra, dix retours depuis l’arrière-plan et récupération après arrêt du processus d’affichage. APK compilé depuis `b38dea0` ; le tag ajoute uniquement un correctif des scripts de test PC.\n\n**Versions :** Vulpes `2.7-preview.15` · Gaia `2.7.0` · Gecko PC `156.0.1` · GeckoView Android `156.0`. Les fonctions matérielles diffèrent selon la plateforme : sous Android, appels et SMS sont délégués aux applications Android, pas au modem Tundra.\n\n**Limites connues :** build de développement signé, débogage Gecko accessible via ADB autorisé ; toutes les versions et tous les modèles Android ne sont pas qualifiés. Sur Sargo : aperçu caméra autour de 16 i/s, flash/vidéo, rotation automatique et veille profonde incomplets, audio des appels à qualifier. Le grand dialogue signalé après un envoi SMS reste à identifier. Cette release n'annonce pas la résolution de ces limites.\n\n## 🇬🇧 Vulpes OS — Preview 15\n\nFirst publication of the **shared desktop / Android / Tundra sources**, with a new experimental APK.\n\n### Download and try\n- **Android:** install `vulpes-os-preview-2.7-preview.15.apk`. Declared minimum Android 8, target API 37; ARM64, ARMv7 and x86_64. Signed with the same key as previous Preview builds.\n- **Linux desktop:** clone the repository, then run `./setup.sh` and `./run.sh`. The pinned Mozilla engine is downloaded and its checksum verified.\n- **Sargo:** Tundra sources are included. **No generic flashable ROM is supplied**: our private image still depends on the development phone's installation and storage layout.\n\n### Since Preview 14\n- Fix SMS date formatting that left Gaia conversations empty; retain messages and sent/failed states in the host.\n- Make camera stream release and replacement reliable, so an older session cannot close the new sensor.\n- Tundra sources: touch focus, digital zoom and sensor JPEG capture, separating preview from full-resolution photos. Fix vibration shutdown and add a service stop safeguard.\n- Publish desktop, APK and Tundra sources with setup/build scripts and updated English/French READMEs. Private profiles and internal working documents are excluded.\n\n**Validation:** 42 service tests, desktop startup/navigation on Gecko 156.0.1; Android 16 emulator Photo/Gallery capture, camera resume, ten background/resume cycles and recovery after renderer termination. APK built from `b38dea0`; the tag adds only a desktop test-script fix.\n\n**Versions:** Vulpes `2.7-preview.15` · Gaia `2.7.0` · desktop Gecko `156.0.1` · Android GeckoView `156.0`. Hardware capabilities differ by platform: Android calls and SMS hand off to Android apps, not to the Tundra modem.\n\n**Known limits:** signed development build, Gecko debugging through authorized ADB; not every Android version/device is qualified. On Sargo: approximately 16 fps camera preview, incomplete flash/video, automatic rotation and deep suspend, call audio awaiting qualification. A reported large dialog after SMS sending remains under investigation. This release does not claim those limits are fixed.\n" + }, + "v2.7-preview.17": { + "version": "2.7-preview.17", + "after": "2.7-preview.15", + "url": "https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.17", + "summary": "# Français\n\nAPK expérimental Android : Vulpes OS 2.7-preview.17, Gaia 2.7.0, GeckoView 156.0. Installation par-dessus une Preview précédente signée avec la même clé. Android 8.0/API26 minimum ; essais réalisés sur émulateur Android 16/API36.\n\n## 2.7-preview.17 — APK construit · 5 octobre 2026\n\n### Commun · Gaia / PC\n\n- Remettre Marketplace sur l’accueil avec son icône originale et le catalogue Vulpes. Ouvrir sa version française pour les langues fr, anglaise sinon.\n- Adapter les téléchargements du catalogue en installations confirmées : vérifier les archives ZIP, ajouter l’application à l’accueil et permettre son lancement et sa désinstallation.\n- Conserver les applications installées séparément des sources Gaia, sur des origines distinctes et sans privilèges système hérités des anciens manifestes.\n- Valider sur PC le parcours réel avec 2048 : catalogue, confirmation, installation, lancement, persistance et désinstallation. Contrôler les archives corrompues, chemins invalides, limites, annulation et confirmations à usage unique.\n\n### Android · APK\n\n- Construire et signer Preview17 avec le même Marketplace et le même service d’installation. Utiliser la vue GeckoView existante pour le catalogue et le stockage privé Android pour les paquets.\n- Vérifier sur émulateur Android 16/API36 le clic Installer, la confirmation, l’apparition sur l’accueil et le lancement de 2048 sans accès au pont système Vulpes. Vérifier aussi la conservation après arrêt forcé et relance, puis la désinstallation.\n\n## 2.7-preview.16 — APK construit · 5 octobre 2026\n\n### Commun · Gaia / PC\n\n- Rétablir les sélecteurs Gaia de fond d’écran et de sons, leur retour vers Paramètres et leur annulation par Accueil. Partager le transport des images et sons enregistrés entre PC et Android.\n- Propager les changements de langue à l’accueil, y compris sans permission de lecture générale des réglages. Vérifier les libellés FR/EN et la persistance.\n- Rétablir l’arrêt du balayage horizontal sur une page et masquer les applications de développement sur l’accueil, sans les supprimer des sources.\n- Utiliser le logo Vulpes fourni dans le splash et adapter les composants originaux de la liste des sons au moteur actuel.\n- Vérifier sur PC deux changements consécutifs de fond, le choix d’un son et les panneaux Gaia restaurés.\n\n### Android · APK\n\n- APK local Preview16 construit et signé ; contenu commun et version vérifiés. Aucune publication dans cette étape.\n- Valider Vulpes comme lanceur HOME, afficher le verrouillage Gaia après veille et rétablir le retour Android depuis la caméra ouverte au verrouillage.\n- Masquer les barres système Android en mode immersif, en conservant leur accès temporaire par balayage.\n- Tests sur émulateur Android 16/API36 : verrouillage, caméra/retour, FR/EN, accueil sans applications de test, fond d’écran et son. Fond conservé après arrêt forcé et relance, sans changement répété pendant 15 secondes. Autres appareils Android non qualifiés dans cette étape.\n\nLes correctifs caméra du 8 octobre ainsi que les derniers correctifs Wi-Fi, bannière et thème Marketplace du 6 octobre ne sont pas inclus dans cet APK. La validation de la caméra avant sur Sargo reste ouverte. Cette publication concerne uniquement Android.\n\n# English\n\nExperimental Android APK: Vulpes OS 2.7-preview.17, Gaia 2.7.0, GeckoView 156.0. Install over an earlier Preview signed with the same key. Android 8.0/API26 minimum; tested on an Android 16/API36 emulator.\n\n## 2.7-preview.17 — APK built · 5 October 2026\n\n### Shared · Gaia / Desktop\n\n- Restore Marketplace on Home with its original icon and the Vulpes catalogue. Select French for fr languages and English otherwise.\n- Turn catalogue downloads into confirmed installations: validate ZIP archives, add the application to Home and support launch and removal.\n- Store installed applications separately from Gaia sources, on distinct origins and without system privileges inherited from archived manifests.\n- Validate the real desktop workflow with 2048: catalogue, confirmation, installation, launch, persistence and removal. Check damaged archives, invalid paths, limits, cancellation and single-use confirmations.\n\n### Android · APK\n\n- Build and sign Preview17 with the same Marketplace and installation service. Use the existing GeckoView browser for the catalogue and private Android storage for packages.\n- Verify the Install click, confirmation, Home icon and 2048 launch without access to the Vulpes system bridge on an Android 16/API36 emulator. Also verify persistence after force-stop/relaunch and removal.\n\n## 2.7-preview.16 — APK built · 5 October 2026\n\n### Shared · Gaia / Desktop\n\n- Restore Gaia wallpaper and sound pickers, return navigation to Settings and cancellation through Home. Share persisted image and sound transport between desktop and Android.\n- Propagate language changes to Home without granting general settings read access. Check French/English labels and persistence.\n- Restore horizontal page snapping and hide development apps from Home without deleting their sources.\n- Use the supplied Vulpes logo in the splash and adapt the original sound-list components to the current engine.\n- Verify two consecutive wallpaper changes, sound selection and restored Gaia panels on desktop.\n\n### Android · APK\n\n- Build and sign local Preview16; verify shared payload and version. Nothing published in this step.\n- Validate Vulpes as the HOME launcher, restore Gaia locking after sleep and Android Back navigation from the lockscreen camera.\n- Hide Android system bars in immersive mode while retaining temporary swipe access.\n- Android 16/API36 emulator checks: locking, camera/Back, French/English, Home without test apps, wallpaper and sound. Wallpaper retained after force-stop/relaunch with no repeated changes over 15 seconds. Other Android devices not qualified in this step.\n\nThe October 8 camera changes and the later October 6 Wi-Fi, banner and Marketplace theme changes are not included in this APK. Front-camera validation on Sargo remains open. This release concerns Android only." } } } diff --git a/gaia/apps/communications/contacts/js/activities.js b/gaia/apps/communications/contacts/js/activities.js index 12cb2a1f..84e333b1 100755 --- a/gaia/apps/communications/contacts/js/activities.js +++ b/gaia/apps/communications/contacts/js/activities.js @@ -19,7 +19,7 @@ const VCARD_MIME_TYPES = [ var ActivityHandler = { _currentActivity: null, - _launchedAsInlineActivity: (window.location.search == '?pick'), + _launchedAsInlineActivity: new URLSearchParams(window.location.search).has('pick'), mozContactParam: null, @@ -124,6 +124,7 @@ var ActivityHandler = { this.launch_activity(activity, 'add-parameters'); break; case 'pick': + if (window.VulpesCompat) this._launchedAsInlineActivity = true; if (!this._launchedAsInlineActivity) { return; } @@ -356,15 +357,18 @@ var ActivityHandler = { postNewSuccess: function ah_postNewSuccess(contact) { this._currentActivity.postResult({ contact: contact }); this._currentActivity = null; + if (window.VulpesCompat) this._launchedAsInlineActivity = false; }, postPickSuccess: function ah_postPickSuccess(result) { this._currentActivity.postResult(result); this._currentActivity = null; + if (window.VulpesCompat) this._launchedAsInlineActivity = false; }, postCancel: function ah_postCancel() { this._currentActivity.postError('canceled'); this._currentActivity = null; + if (window.VulpesCompat) this._launchedAsInlineActivity = false; } }; diff --git a/gaia/apps/communications/contacts/js/contacts.js b/gaia/apps/communications/contacts/js/contacts.js index 64dc91a5..4ea36df1 100755 --- a/gaia/apps/communications/contacts/js/contacts.js +++ b/gaia/apps/communications/contacts/js/contacts.js @@ -655,7 +655,9 @@ var Contacts = (function() { utils.PerformanceHelper.contentInteractive(); utils.PerformanceHelper.chromeInteractive(); window.setTimeout(Contacts && Contacts.onLocalized); - if (window.navigator.mozSetMessageHandler && window.self == window.top) { + // Vulpes embeds each app in its own permission-checked frame. + if (window.navigator.mozSetMessageHandler && + (window.self == window.top || window.VulpesCompat)) { LazyLoader.load(['/shared/js/contacts/import/utilities/misc.js', '/shared/js/contacts/import/utilities/vcard_reader.js', '/shared/js/contacts/import/utilities/vcard_parser.js'], diff --git a/gaia/apps/communications/contacts/js/views/form.js b/gaia/apps/communications/contacts/js/views/form.js index 865eead5..ef5e805c 100755 --- a/gaia/apps/communications/contacts/js/views/form.js +++ b/gaia/apps/communications/contacts/js/views/form.js @@ -1038,6 +1038,8 @@ contacts.Form = (function() { // (Bug 1041455). if (editingContact) { Contacts.setCurrent(contact); + // The change event may arrive while the edit form is still visible. + Contacts.updateContactDetail(contact.id); } } ); diff --git a/gaia/apps/homescreen/js/apps.js b/gaia/apps/homescreen/js/apps.js index 9a982135..e7725e97 100755 --- a/gaia/apps/homescreen/js/apps.js +++ b/gaia/apps/homescreen/js/apps.js @@ -408,6 +408,10 @@ return; } + if (HIDDEN_ROLES.includes(manifest.role)) { + return; + } + // Do not add blacklisted apps if (BLACKLIST.includes(app.origin)) { return; diff --git a/gaia/apps/marketplace/icon.png b/gaia/apps/marketplace/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9880f58aec887a6796c53cc02872398d658c680e GIT binary patch literal 190651 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 literal 0 HcmV?d00001 diff --git a/gaia/apps/marketplace/index.html b/gaia/apps/marketplace/index.html new file mode 100644 index 00000000..bee23bd1 --- /dev/null +++ b/gaia/apps/marketplace/index.html @@ -0,0 +1,5 @@ +Marketplace + + + + diff --git a/gaia/apps/marketplace/manifest.webapp b/gaia/apps/marketplace/manifest.webapp new file mode 100644 index 00000000..8cded036 --- /dev/null +++ b/gaia/apps/marketplace/manifest.webapp @@ -0,0 +1,19 @@ +{ + "name": "Marketplace", + "description": "Vulpes OS application catalogue", + "launch_path": "/index.html", + "type": "certified", + "icons": { + "512": "/icon.png" + }, + "permissions": { + "browser": {}, + "webapps-manage": {} + }, + "locales": { + "fr": { + "name": "Marketplace", + "description": "Catalogue des applications Vulpes OS" + } + } +} diff --git a/gaia/apps/marketplace/marketplace.css b/gaia/apps/marketplace/marketplace.css new file mode 100644 index 00000000..0125ce10 --- /dev/null +++ b/gaia/apps/marketplace/marketplace.css @@ -0,0 +1 @@ +html,body{width:100%;height:100%;margin:0;background:#f4f4f4;font-family:Fira Sans,sans-serif}#catalogue{position:absolute;inset:0;width:100%;height:100%;border:0}#message{padding:3rem 2rem;text-align:center;font-size:1.8rem}#message button{display:block;width:100%;padding:1rem;margin:1rem 0;font:inherit}#open[hidden],[hidden]{display:none!important}#confirm{z-index:10}#confirm section{overflow:auto}#permissions{font-size:1.5rem} diff --git a/gaia/apps/marketplace/marketplace.js b/gaia/apps/marketplace/marketplace.js new file mode 100644 index 00000000..84dbc0c5 --- /dev/null +++ b/gaia/apps/marketplace/marketplace.js @@ -0,0 +1,32 @@ +(async()=>{ + const frame=document.getElementById('catalogue'),message=document.getElementById('message'),dialog=document.getElementById('confirm'); + let attempt=0,busy=false,prepared=null,installed=null,language=await VulpesCompat.call('locale.current'); + const fr=()=>/^fr(?:-|$)/i.test(language); + const text=(f,e)=>fr()?f:e; + function labels(){document.getElementById('browse').textContent=text('Ouvrir le catalogue','Open catalogue');document.getElementById('open').textContent=text('Ouvrir','Open');document.getElementById('install').textContent=text('Installer','Install');document.getElementById('cancel').textContent=text('Annuler','Cancel');} + function loadFailed(){status(text('Catalogue inaccessible. Vérifiez la connexion Internet, puis réessayez.','Catalogue unavailable. Check your Internet connection, then try again.'));document.getElementById('open').hidden=true;} + frame.addEventListener('mozbrowsererror',loadFailed); + function browse(){labels();message.hidden=true;dialog.hidden=true;frame.style.visibility='visible';frame.src='https://vulpes-os.org/marketplace/?mode=app&lang='+(fr()?'fr':'en');} + function status(value){frame.style.visibility='hidden';message.hidden=false;document.getElementById('status').textContent=value;} + async function prepare(url){ + if(busy)return;const current=++attempt;busy=true;prepared=null;installed=null;document.getElementById('open').hidden=true; + status(text('Vérification de l’application…','Checking application…')); + try {const value=await VulpesCompat.call('marketplace.prepare',{url});if(current!==attempt)return;prepared=value; + document.getElementById('title').textContent=prepared.name; + document.getElementById('question').textContent=text('Installer cette application archivée ?','Install this archived application?')+' ('+(prepared.bytes/1048576).toFixed(1)+' Mio)'; + document.getElementById('permissions').textContent=text('Elle sera exécutée comme une application web, sans accès aux fonctions système. Certains anciens services peuvent ne plus fonctionner.','It will run as a web application without system access. Some old services may no longer work.'); + message.hidden=true;dialog.hidden=false; + }catch(error){if(current!==attempt)return;status(text('Installation impossible : ','Cannot install: ')+error.message);busy=false;} + } + dialog.addEventListener('submit',async event=>{event.preventDefault();if(!prepared)return;const token=prepared.token;prepared=null;dialog.hidden=true;status(text('Installation…','Installing…')); + try {installed=await VulpesCompat.call('marketplace.install',{token});status(text('Application ajoutée à l’accueil.','Application added to Home.'));document.getElementById('open').hidden=false;} + catch(error){status(text('Installation impossible : ','Cannot install: ')+error.message);}finally{busy=false;} + }); + const cancel=()=>{attempt++;prepared=null;busy=false;VulpesCompat.call('marketplace.cancel').catch(console.error);browse();}; + document.getElementById('cancel').onclick=cancel; + document.getElementById('browse').onclick=cancel; + document.getElementById('open').onclick=()=>installed && VulpesCompat.call('apps.launch',{manifestURL:installed.manifestURL}); + addEventListener('languagechange',()=>{language=navigator.language;if(!busy)browse();}); + browse(); + setInterval(async()=>{if(busy)return;try{const url=await VulpesCompat.call('marketplace.poll');if(url)await prepare(url);}catch(error){console.error(error);}},750); +})(); diff --git a/gaia/apps/ringtones/js/tone_player.js b/gaia/apps/ringtones/js/tone_player.js index aa92cd42..97358a05 100755 --- a/gaia/apps/ringtones/js/tone_player.js +++ b/gaia/apps/ringtones/js/tone_player.js @@ -143,7 +143,7 @@ TonePlayer.prototype = { _setExclusiveMode: function(exclusive) { if (exclusive) { if (!this._source) { - this._context = new AudioContext('ringer'); + this._context = new AudioContext(); this._source = this._context.createMediaElementSource(this._player); this._source.connect(this._context.destination); } diff --git a/gaia/apps/settings/js/modules/page_transitions.js b/gaia/apps/settings/js/modules/page_transitions.js index 11951804..59f84c11 100755 --- a/gaia/apps/settings/js/modules/page_transitions.js +++ b/gaia/apps/settings/js/modules/page_transitions.js @@ -43,7 +43,39 @@ define(function() { return; } + // A hidden or newly inserted panel may not animate. Complete navigation + // in that case too, otherwise its onShow hook never runs. + var completed = false; + var fallback; + function paintWait(event) { + if (event && event.target !== newPanel) { + return; + } + if (completed) { + return; + } + completed = true; + clearTimeout(fallback); + newPanel.removeEventListener('transitionend', paintWait); + newPanel.removeEventListener('transitioncancel', paintWait); + _sendPanelReady(oldPanel && '#' + oldPanel.id, '#' + newPanel.id); + if (callback) { + callback(); + } + } + newPanel.addEventListener('transitionend', paintWait); + newPanel.addEventListener('transitioncancel', paintWait); newPanel.className = 'current'; + var style = window.getComputedStyle(newPanel); + var seconds = function(value) { + return parseFloat(value) * (value.trim().endsWith('ms') ? 1 : 1000) || 0; + }; + var durations = style.transitionDuration.split(',').map(seconds); + var delays = style.transitionDelay.split(',').map(seconds); + var duration = Math.max.apply(Math, durations.map(function(value, index) { + return value + delays[index % delays.length]; + })); + fallback = setTimeout(paintWait, Math.max(0, duration) + 100); /** * Most browsers now scroll content into view taking CSS transforms into @@ -57,22 +89,6 @@ define(function() { window.scrollTo(0, 0); } - newPanel.addEventListener('transitionend', function paintWait() { - newPanel.removeEventListener('transitionend', paintWait); - if (oldPanel) { - _sendPanelReady('#' + oldPanel.id, '#' + newPanel.id); - - if (oldPanel.className === 'current') { - return; - } - } else { - _sendPanelReady(null, '#' + newPanel.id); - } - - if (callback) { - callback(); - } - }); }, /** diff --git a/gaia/apps/system/js/system_banner.js b/gaia/apps/system/js/system_banner.js index 943eadaf..c88f6b55 100755 --- a/gaia/apps/system/js/system_banner.js +++ b/gaia/apps/system/js/system_banner.js @@ -10,7 +10,9 @@ * application installation, crash reporter, and low storage notices. * @class SystemBanner */ - function SystemBanner() {} + function SystemBanner() { + this._generation = 0; + } /** * Amount of time before banner is automatically dismissed. @@ -77,10 +79,18 @@ * { label: l10nAttrs, callback: ..., dismiss: ... } */ show: function(messages, buttonParams) { + var generation = ++this._generation; + clearTimeout(this._dismissTimer); + if (this._cancelRemoval) { + this._cancelRemoval(); + } this._clicked = false; this._showing = true; return this.getBanner().then(banner => { + if (generation !== this._generation || !this._showing) { + return; + } // Clear the button and message containers. var messageContainer = banner.querySelector('.messages'); var buttonContainer = banner.querySelector('.buttons'); @@ -110,13 +120,16 @@ // Hide banner and invoke button dismiss() on click or timeout. var hideBanner = () => { + if (generation !== this._generation || !this._showing) { + return; + } this.hide(); if (!this._clicked) { buttonParams && buttonParams.dismiss && buttonParams.dismiss(); } }; banner.onclick = hideBanner; - setTimeout(hideBanner, SystemBanner.TIMEOUT); + this._dismissTimer = setTimeout(hideBanner, SystemBanner.TIMEOUT); document.getElementById('screen').appendChild(banner); banner.show(); @@ -131,15 +144,37 @@ return; } this._showing = false; + clearTimeout(this._dismissTimer); + var generation = this._generation; return this.getBanner().then(banner => { - banner.addEventListener('animationend', function removeBanner() { - banner.removeEventListener('animationend', removeBanner); - // Only remove banner if we haven't called show() in the meantime. - if (!this._showing) { + if (generation !== this._generation || this._showing) { + return; + } + // Animation events inside a modern shadow root do not reach the host. + var animation = banner.shadowRoot && + banner.shadowRoot.querySelector('.bread') || banner; + var cleanup = () => { + clearTimeout(this._removeTimer); + animation.removeEventListener('animationend', removeBanner); + animation.removeEventListener('animationcancel', removeBanner); + this._cancelRemoval = null; + }; + var removeBanner = event => { + if (event && event.target !== animation) { + return; + } + cleanup(); + if (!this._showing && generation === this._generation) { banner.parentNode && banner.parentNode.removeChild(banner); } - }.bind(this)); + }; + this._cancelRemoval = cleanup; + animation.addEventListener('animationend', removeBanner); + animation.addEventListener('animationcancel', removeBanner); + // Gaia's exit animation lasts 600 ms. Also dismiss if it is disabled + // or no animation event is produced. + this._removeTimer = setTimeout(removeBanner, 700); banner.hide(); }); } diff --git a/host/Runtime.sys.mjs b/host/Runtime.sys.mjs index 0b11845d..e328a542 100644 --- a/host/Runtime.sys.mjs +++ b/host/Runtime.sys.mjs @@ -1,6 +1,8 @@ -import { setInterval } from 'resource://gre/modules/Timer.sys.mjs'; +import { setInterval, setTimeout, clearTimeout } from 'resource://gre/modules/Timer.sys.mjs'; import {Input} from 'resource://vulpes/host/Input.sys.mjs'; import { ScreenPower } from 'resource://vulpes/services/screen-power.mjs'; +import { Packages } from 'resource://vulpes/services/packages.mjs'; +import { Activities, isGaiaActivity } from 'resource://vulpes/services/activities.mjs'; import { SettingsService } from 'resource://vulpes/services/settings.mjs'; import { DataStoreService } from 'resource://vulpes/services/datastores.mjs'; import { DataStoresIndexedDB } from 'resource://vulpes/adapters/datastores-indexeddb.mjs'; @@ -29,6 +31,7 @@ export const Runtime = { wakeLocks: new Map(), idleListeners: new Map(), cleanup(actor) { + this.activities?.cleanup(actor); this.input?.cleanup(actor); this.platform?.nativeCamera.cleanup(actor); this.connections?.cleanup(actor); @@ -45,6 +48,22 @@ export const Runtime = { this.input = new Input(this); const root = Services.env.get('VULPES_WORKSPACE_ROOT'); this.apps = await IOUtils.readJSON(PathUtils.join(root, 'assets', 'registry.json')); + const storeURL = 'http://system.localhost:8765/_vulpes/packages/'; + const store = async (operation, item) => { + const token = await IOUtils.readUTF8(PathUtils.join(root,'profiles','installed-apps','.token')); + const response = await fetch(storeURL+operation,{method:'POST',headers:{'X-Vulpes-Install':token,'Content-Type':'application/json'},body:JSON.stringify(item)}); + if(!response.ok)throw Error('PACKAGE_STORAGE_FAILED'); + return response.json(); + }; + this.refreshInstalled = async () => { + const response=await fetch(storeURL+'list');if(!response.ok)throw Error('PACKAGE_STORAGE_FAILED'); + const installed=await response.json(); + this.apps.splice(0,this.apps.length,...this.apps.filter(a=>!a.installed),...installed); + return installed; + }; + this.packages = new Packages({schedule:setTimeout,unschedule:clearTimeout,save:item=>store('save',item),changed:()=>this.refreshInstalled(),uuid:()=>Services.uuid.generateUUID().toString()}); + this.removePackage = async id => {const app=this.apps.find(a=>a.id===id && a.installed);if(!app)throw Error('NOT_REMOVABLE'); + await store('remove',{id});await this.refreshInstalled();this.broadcast('apps-uninstalled',app);}; // Installed Gaia audio apps must be able to ring when an alarm fires. This // permission is scoped to their origins in this profile, never web pages. for (const app of this.apps) @@ -91,6 +110,11 @@ export const Runtime = { this.values['vulpes.gaia-restoration.v1'] = true; } Services.prefs.setStringPref('intl.accept_languages', this.values['language.current']); + this.activities = new Activities({identity:actor=>this.identity(actor),apps:()=>this.apps, + uuid:()=>Services.uuid.generateUUID().toString(), + notify:(actor,type,data)=>actor.sendAsyncMessage('Vulpes:Event',{type,data}), + launch:(target,path,{reload=false,entryPoint}={})=>this.broadcast(reload?'activity-launch':'launch',{manifestURL:target.manifestURL,origin:target.origin, + entryPoint,url:new URL(path || target.manifest.launch_path || '/index.html',target.origin).href,timestamp:Date.now()},'system')}); this.writeQueue = Promise.resolve(); this.settings = new SettingsService({ get: async (key) => this.values[key], @@ -115,7 +139,7 @@ export const Runtime = { this.writeQueue = write.catch(() => {}); return write; }, - }); + }, {maxBytes:32 * 1024 * 1024}); const storage = new DataStoresIndexedDB(indexedDB, IDBKeyRange, () => Services.uuid.generateUUID().toString(), ); @@ -202,6 +226,16 @@ export const Runtime = { this.actors.add(actor); const { method, params } = request; if (typeof method !== 'string') return { error: { code: 'INVALID_REQUEST' } }; + if(method.startsWith('marketplace.')) { + if(app.id!=='marketplace')return {error:{code:'PERMISSION_DENIED'}}; + try { + if(method==='marketplace.poll'){const url=this.marketplaceDownload;this.marketplaceDownload=null;return {result:url||null};} + if(method==='marketplace.cancel'){this.packages.cancel();return {result:true};} + if(method==='marketplace.prepare')return {result:await this.packages.prepare(params.url)}; + if(method==='marketplace.install') {const id=await this.packages.install(params.token);const installed=this.apps.find(a=>a.id===id);this.broadcast('apps-installed',installed);return {result:installed};} + } catch(error) {return {error:{code:error.message}};} + return {error:{code:'METHOD_NOT_SUPPORTED'}}; + } if(method==='hardware.vibrate') { if(app.id!=='system' || Services.env.get('VULPES_TUNDRA')!=='1') return {error:{code:'NOT_SUPPORTED'}}; const pattern=params?.pattern; @@ -267,10 +301,19 @@ export const Runtime = { } } if (typeof method !== 'string') return { error: { code: 'INVALID_REQUEST' } }; + if (['activities.subscribe','activities.finish','activities.cancel'].includes(method) || (method === 'activities.start' && isGaiaActivity(params))) { + try { + const result = method === 'activities.cancel' ? this.activities.cancel(actor) + : method === 'activities.subscribe' ? this.activities.subscribe(actor) + : method === 'activities.finish' ? this.activities.finish(actor,params) + : await this.activities.start(actor,params); + return {result:result ?? null}; + } catch(error) {return {error:{code:error.message}};} + } if (method === 'activities.start') { if (params?.name === 'record' && params.data?.type === 'photos') { - // Gallery's Photo button opens the shared Gaia UI; it expects no result file. - if (app.id === 'gallery') { + // Gallery and the lockscreen open Gaia Camera without expecting a file result. + if (['gallery', 'system'].includes(app.id)) { const camera = this.apps.find((a) => a.id === 'camera'); if (!camera) return { error: { code: 'ACTIVITY_NOT_SUPPORTED' } }; this.broadcast( @@ -316,6 +359,7 @@ export const Runtime = { return { error: { code: error.message || 'VIEW_ERROR' } }; } } + if (method === 'locale.current') return { result: this.values['language.current'] || 'en-US' }; if (method === 'settings.snapshot') return this.grants(app).includes('settings.read') ? { result: { ...this.values } } @@ -441,6 +485,10 @@ export const Runtime = { if (method === 'apps.self') return { result: app }; if (method === 'apps.list') return manage ? { result: this.apps } : { error: { code: 'PERMISSION_DENIED' } }; + if(method==='apps.uninstall') { + if(!manage)return {error:{code:'PERMISSION_DENIED'}}; + try {await this.removePackage(params.id);return {result:true};}catch(error){return {error:{code:error.message}};} + } if (method === 'apps.launch') { const target = this.apps.find((a) => a.manifestURL === params?.manifestURL); if (!target || (!manage && app.id !== target.id)) @@ -469,7 +517,7 @@ export const Runtime = { try { const app = this.identity(actor); if (!app || (target && app.id !== target)) continue; - if (type === 'settingchange' && !this.grants(app).includes('settings.read')) continue; + if (type === 'settingchange' && data.key !== 'language.current' && !this.grants(app).includes('settings.read')) continue; if ( type === 'mediachange' && !app.manifest.permissions?.['device-storage:' + data.type]?.access?.includes('read') diff --git a/host/Views.sys.mjs b/host/Views.sys.mjs index 20ebed76..3884c1cd 100644 --- a/host/Views.sys.mjs +++ b/host/Views.sys.mjs @@ -15,12 +15,52 @@ export const Views = { ); Services.ppmm.sharedData.flush(); }, + hideAll() { + for (const entry of this.entries.values()) entry.browser.style.visibility='hidden'; + }, + async layout(entry) { + const geometry=entry.geometry; + if (!geometry) return; + let {left,top,width,height,visible}=geometry; + let right=left+width,bottom=top+height; + try { + // A child document can remain visible while its owning Gaia app is hidden. + // Check every embedding frame in its own process, up to the system app. + for (let child=entry.actor.browsingContext;child.parent;child=child.parent) { + const state=await child.parent.currentWindowGlobal.getActor('Vulpes').sendQuery( + 'Vulpes:ViewState',{contextId:child.id}); + visible=visible && state.visible; + if (!visible) break; + left=Math.max(left,state.left);top=Math.max(top,state.top); + right=Math.min(right,state.right);bottom=Math.min(bottom,state.bottom); + } + } catch (_) {visible=false;} + if (entry.geometry!==geometry || !entry.browser.isConnected) return; + const shell=entry.browser.ownerDocument.defaultView; + const bounds=shell.document.getElementById('screen').getBoundingClientRect(); + const x=shell.mozInnerScreenX+bounds.left,y=shell.mozInnerScreenY+bounds.top; + left=Math.max(x,left);top=Math.max(y,top); + right=Math.min(x+bounds.width,right);bottom=Math.min(y+bounds.height,bottom); + Object.assign(entry.browser.style,{left:(left-x)+'px',top:(top-y)+'px', + width:Math.max(0,right-left)+'px',height:Math.max(0,bottom-top)+'px', + visibility:visible && right>left && bottom>top?'visible':'hidden'}); + }, prune() { for (const [id, entry] of this.entries) { if (!BrowsingContext.get(id)?.parent) { this.remove(id, entry); continue; } + this.layout(entry); + const document=entry.browser.browsingContext?.currentWindowGlobal; + const page=document?.documentURI.spec || ''; + // A retry keeps the previous error document until the next navigation commits. + if (document?.innerWindowId!==entry.previousDocumentId && + /^about:(neterror|certerror|tabcrashed)/.test(page) && entry.errorPage!==page) { + entry.errorPage=page; + try {entry.actor.sendAsyncMessage('Vulpes:Event',{type:'view', + data:{contextId:id,type:'error',detail:{type:'other'}}});} catch (_) {} + } // A detached iframe's BrowsingContext can outlive its DOM element. // Ask the trusted child actor about actual ownership, not just the BC tree. if (entry.checking) continue; @@ -123,7 +163,9 @@ export const Views = { if (flags & Ci.nsIWebProgressListener.STATE_STOP) { emit('loadend', { backgroundColor: 'rgb(255, 255, 255)' }); emit('titlechange', browser.contentTitle || browser.currentURI.spec); - if (!Components.isSuccessCode(status)) emit('error', { type: 'other', status }); + // Replacing a navigation cancels the previous request normally. + if (!Components.isSuccessCode(status) && status !== Cr.NS_BINDING_ABORTED) + emit('error', { type: 'other', status }); } }, onProgressChange() {}, @@ -143,6 +185,8 @@ export const Views = { this.entries.set(contextId, entry); } entry.url = url; + entry.errorPage=null; + entry.previousDocumentId=entry.browser.browsingContext?.currentWindowGlobal?.innerWindowId; entry.browser.loadURI(uri, { triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), }); @@ -153,22 +197,8 @@ export const Views = { if (operation === 'geometry') { const { left, top, width, height, visible } = geometry || {}; if (![left, top, width, height].every(Number.isFinite)) throw new Error('INVALID_GEOMETRY'); - const shell = browser.ownerDocument.defaultView, - screen = shell.document.getElementById('screen'); - const bounds = screen.getBoundingClientRect(); - const x = left - shell.mozInnerScreenX - bounds.left; - const y = top - shell.mozInnerScreenY - bounds.top; - const right = Math.min(480, x + width), - bottom = Math.min(800, y + height); - const clippedX = Math.max(0, x), - clippedY = Math.max(0, y); - Object.assign(browser.style, { - left: clippedX + 'px', - top: clippedY + 'px', - width: Math.max(0, right - clippedX) + 'px', - height: Math.max(0, bottom - clippedY) + 'px', - visibility: visible && right > clippedX && bottom > clippedY ? 'visible' : 'hidden', - }); + entry.geometry={left,top,width,height,visible}; + await this.layout(entry); return null; } if (operation === 'back') { diff --git a/host/actors/MarketplaceChild.sys.mjs b/host/actors/MarketplaceChild.sys.mjs new file mode 100644 index 00000000..7181ffd4 --- /dev/null +++ b/host/actors/MarketplaceChild.sys.mjs @@ -0,0 +1,7 @@ +export class VulpesMarketplaceChild extends JSWindowActorChild { + handleEvent() { + const scope={}; + Services.scriptloader.loadSubScript('resource://vulpes/host/marketplace-page.js',scope); + scope.installMarketplacePage(this.contentWindow,url=>this.sendAsyncMessage('Marketplace:Download',{url})); + } +} diff --git a/host/actors/MarketplaceParent.sys.mjs b/host/actors/MarketplaceParent.sys.mjs new file mode 100644 index 00000000..f59695ca --- /dev/null +++ b/host/actors/MarketplaceParent.sys.mjs @@ -0,0 +1,13 @@ +import { Runtime } from 'resource://vulpes/host/Runtime.sys.mjs'; +import { Views } from 'resource://vulpes/host/Views.sys.mjs'; +import { packageURL } from 'resource://vulpes/services/packages.mjs'; +export class VulpesMarketplaceParent extends JSWindowActorParent { + receiveMessage(message) { + if(message.name!=='Marketplace:Download' || this.manager.documentPrincipal.originNoSuffix!=='https://vulpes-os.org')return; + const view=[...Views.entries.values()].find(v=>v.browser.browsingContext.top.id===this.browsingContext.top.id); + if(!view || !Runtime.identity(view.actor))return; + Runtime.marketplaceDownload=packageURL(message.data.url); + const app=Runtime.apps.find(a=>a.id==='marketplace'); + Runtime.broadcast('launch',{manifestURL:app.manifestURL,origin:app.origin,url:app.origin+'/index.html',timestamp:Date.now()},'system'); + } +} diff --git a/host/actors/VulpesChild.sys.mjs b/host/actors/VulpesChild.sys.mjs index ed38d710..c69ed27f 100644 --- a/host/actors/VulpesChild.sys.mjs +++ b/host/actors/VulpesChild.sys.mjs @@ -141,6 +141,27 @@ export class VulpesChild extends JSWindowActorChild { } receiveMessage(message) { if(message.name==='Vulpes:Edit') return this.edit(message.data); + if (message.name === 'Vulpes:ViewState') { + const frame = Array.from(this.document.querySelectorAll('iframe')).find( + f => f.browsingContext?.id === message.data.contextId); + if (!frame?.isConnected) return {visible:false}; + const win = this.contentWindow, rect = frame.getBoundingClientRect(); + const visible = !this.document.hidden && frame.checkVisibility({checkOpacity:true,checkVisibilityCSS:true}) && + [[.5,.5],[.05,.05],[.95,.05],[.05,.95],[.95,.95]].every(([x,y]) => + this.document.elementFromPoint(rect.left+rect.width*x,rect.top+rect.height*y)===frame); + let top = Math.max(0,rect.top); + // Gaia's status bar belongs above all app content, including native web views. + if (this.document.location.hostname === 'system.localhost') { + const bar=this.document.getElementById('statusbar'); + if (bar?.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})) { + const r=bar.getBoundingClientRect(); + if (r.width>0 && r.bottom>top && r.top<=top) top=r.bottom; + } + } + return {visible,left:Math.max(0,rect.left)+win.mozInnerScreenX, + top:top+win.mozInnerScreenY,right:Math.min(win.innerWidth,rect.right)+win.mozInnerScreenX, + bottom:Math.min(win.innerHeight,rect.bottom)+win.mozInnerScreenY}; + } if (message.name === 'Vulpes:ViewAlive') { return Array.from(this.document.querySelectorAll('iframe')).some( (frame) => frame.isConnected && frame.browsingContext?.id === message.data.contextId, diff --git a/host/desktop.css b/host/desktop.css index d7e4cb4b..40a36351 100644 --- a/host/desktop.css +++ b/host/desktop.css @@ -196,3 +196,7 @@ html[data-vulpes-app='communications'] body { /* Camera gestures must not magnify the application chrome. */ html[data-vulpes-app='camera'], html[data-vulpes-app='camera'] body { touch-action:none; } + +/* Homescreen pages must settle at their boundary after a drag (Forgejo #3). */ +html[data-vulpes-app="homescreen"] #panels { scroll-snap-type:x mandatory; } +html[data-vulpes-app="homescreen"] #panels > * { scroll-snap-align:start; } diff --git a/host/gaia-camera.js b/host/gaia-camera.js index 304c704a..b056ff2d 100644 --- a/host/gaia-camera.js +++ b/host/gaia-camera.js @@ -6,6 +6,26 @@ current = null, generation = 0; let cameraError = null; + let opening = Promise.resolve(), closing = Promise.resolve(); + const releases = new WeakMap(); + function releaseStream(stream, preview) { + if (releases.has(stream)) return releases.get(stream); + if (current === stream) current = null; + const release = Promise.resolve().then(async () => { + for (const video of document.querySelectorAll('video')) { + if (video.srcObject === stream) { + video.pause(); video.srcObject = null; + if ('vulpesCapture' in video.dataset) video.remove(); + } + } + preview?.remove(); + try { await stream.vulpesCameraClose?.(); } + finally { stream.getTracks().forEach(track => track.stop()); } + }); + releases.set(stream, release); + closing = release.catch(recordError); + return release; + } function recordError(error) { cameraError = { name: error.name || 'Error', message: error.message || '' }; console.warn('Vulpes Camera:', cameraError.name, cameraError.message); @@ -87,7 +107,7 @@ Object.assign(event, detail); target.dispatchEvent(event); } - async function discover(stream) { + async function discover(stream, name) { if (stream.vulpesNative) { cameras = ['back', 'front']; return; @@ -104,12 +124,15 @@ const active = list.find((d) => d.deviceId === settings.deviceId) || list[0]; const back = list.find((d) => /back|rear|environment/i.test(d.label)); const front = list.find((d) => /front|user|face/i.test(d.label)); - const first = - back || - (settings.facingMode === 'user' && list.length > 1 ? list.find((d) => d !== active) : active); + // Unlabelled webcams must not exchange identities every time we switch. + const previousBack = list.find(d => d.deviceId === devices.get('back')); + const previousFront = list.find(d => d.deviceId === devices.get('front')); + const first = back || previousBack || + ((settings.facingMode === 'user' || name === 'front') && list.length > 1 + ? list.find(d => d !== active) : active); devices = new Map(); if (first) devices.set('back', first.deviceId); - const second = front && front !== first ? front : list.find((d) => d !== first); + const second = [front, previousFront].find(d => d && d !== first) || list.find(d => d !== first); if (second) devices.set('front', second.deviceId); cameras = devices.size ? [...devices.keys()] : ['back']; } @@ -129,75 +152,79 @@ await call({action:'stop'}); throw new DOMException('Camera preview requires WebGL', 'NotSupportedError'); } - function shader(type, source) { - const value = gl.createShader(type); - gl.shaderSource(value, source); gl.compileShader(value); - if (!gl.getShaderParameter(value, gl.COMPILE_STATUS)) throw Error('Camera shader compilation failed'); - return value; - } - const program = gl.createProgram(); - gl.attachShader(program, shader(gl.VERTEX_SHADER, ` - attribute vec2 position; varying vec2 coord; - void main() { gl_Position=vec4(position,0.,1.); coord=vec2(position.x*.5+.5,.5-position.y*.5); } - `)); - gl.attachShader(program, shader(gl.FRAGMENT_SHADER, ` - precision mediump float; varying vec2 coord; - uniform sampler2D luma; uniform sampler2D chroma; uniform float rotation; - void main() { - vec2 p=coord; - if(rotation==90.) p=vec2(coord.y,1.-coord.x); - else if(rotation==270.) p=vec2(1.-coord.y,coord.x); - else if(rotation==180.) p=1.-coord; - float y=1.16438356*(texture2D(luma,p).r-16./255.); - vec4 vu=texture2D(chroma,p); - float v=vu.r-128./255.; float u=vu.a-128./255.; - gl_FragColor=vec4(y+1.59602678*v,y-.39176229*u-.81296764*v,y+2.01723214*u,1.); - } - `)); - gl.linkProgram(program); - if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw Error('Camera shader link failed'); - gl.useProgram(program); - const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER,buffer); - gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]),gl.STATIC_DRAW); - const position=gl.getAttribLocation(program,'position'); - gl.enableVertexAttribArray(position);gl.vertexAttribPointer(position,2,gl.FLOAT,false,0,0); - gl.uniform1f(gl.getUniformLocation(program,'rotation'),meta.orientation); - const textures = ['luma','chroma'].map((name,index) => { - const texture=gl.createTexture();gl.activeTexture(gl.TEXTURE0+index);gl.bindTexture(gl.TEXTURE_2D,texture); - gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE); - gl.uniform1i(gl.getUniformLocation(program,name),index);return texture; - }); const timing = {frames:0, startedAt:performance.now(), conversionMs:0}; - let stopped = false, timer, stream, sequence = -1, closing; + let stopped = false, timer, stream, sequence = -1, closing, program, buffer; + const textures = [], shaders = []; const stop = () => { if (closing) return closing; stopped = true; clearTimeout(timer); closing = call({action:'stop'}).finally(() => { textures.forEach(texture => gl.deleteTexture(texture)); - gl.deleteBuffer(buffer); gl.deleteProgram(program); + shaders.forEach(value => gl.deleteShader(value)); + if (buffer) gl.deleteBuffer(buffer); + if (program) gl.deleteProgram(program); gl.getExtension('WEBGL_lose_context')?.loseContext(); }); return closing; }; - const draw = async () => { - const frame = await call({action:'frame'}); - if (stopped || !frame.data || frame.sequence === sequence) return false; - const data = new Uint8Array(frame.data), w = meta.width, h = meta.height; - if (data.length !== w * h * 3 / 2) throw Error('Invalid native camera frame'); - sequence = frame.sequence; - const start=performance.now(); - gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,textures[0]); - gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,w,h,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,data.subarray(0,w*h)); - gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D,textures[1]); - gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE_ALPHA,w/2,h/2,0,gl.LUMINANCE_ALPHA,gl.UNSIGNED_BYTE,data.subarray(w*h)); - gl.drawArrays(gl.TRIANGLE_STRIP,0,4); - timing.frames++;timing.conversionMs+=performance.now()-start; - return true; - }; try { + function shader(type, source) { + const value = gl.createShader(type); + shaders.push(value); + gl.shaderSource(value, source); gl.compileShader(value); + if (!gl.getShaderParameter(value, gl.COMPILE_STATUS)) throw Error('Camera shader compilation failed'); + return value; + } + program = gl.createProgram(); + gl.attachShader(program, shader(gl.VERTEX_SHADER, ` + attribute vec2 position; varying vec2 coord; + void main() { gl_Position=vec4(position,0.,1.); coord=vec2(position.x*.5+.5,.5-position.y*.5); } + `)); + gl.attachShader(program, shader(gl.FRAGMENT_SHADER, ` + precision mediump float; varying vec2 coord; + uniform sampler2D luma; uniform sampler2D chroma; uniform float rotation; + void main() { + vec2 p=coord; + if(rotation==90.) p=vec2(coord.y,1.-coord.x); + else if(rotation==270.) p=vec2(1.-coord.y,coord.x); + else if(rotation==180.) p=1.-coord; + float y=1.16438356*(texture2D(luma,p).r-16./255.); + vec4 vu=texture2D(chroma,p); + float v=vu.r-128./255.; float u=vu.a-128./255.; + gl_FragColor=vec4(y+1.59602678*v,y-.39176229*u-.81296764*v,y+2.01723214*u,1.); + } + `)); + gl.linkProgram(program); + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw Error('Camera shader link failed'); + gl.useProgram(program); + buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER,buffer); + gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]),gl.STATIC_DRAW); + const position=gl.getAttribLocation(program,'position'); + gl.enableVertexAttribArray(position);gl.vertexAttribPointer(position,2,gl.FLOAT,false,0,0); + gl.uniform1f(gl.getUniformLocation(program,'rotation'),meta.orientation); + ['luma','chroma'].forEach((name,index) => { + const texture=gl.createTexture();textures.push(texture);gl.activeTexture(gl.TEXTURE0+index);gl.bindTexture(gl.TEXTURE_2D,texture); + gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE); + gl.uniform1i(gl.getUniformLocation(program,name),index); + }); + const draw = async () => { + const frame = await call({action:'frame'}); + if (stopped || !frame.data || frame.sequence === sequence) return false; + const data = new Uint8Array(frame.data), w = meta.width, h = meta.height; + if (data.length !== w * h * 3 / 2) throw Error('Invalid native camera frame'); + sequence = frame.sequence; + const start=performance.now(); + gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,textures[0]); + gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,w,h,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,data.subarray(0,w*h)); + gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D,textures[1]); + gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE_ALPHA,w/2,h/2,0,gl.LUMINANCE_ALPHA,gl.UNSIGNED_BYTE,data.subarray(w*h)); + gl.drawArrays(gl.TRIANGLE_STRIP,0,4); + timing.frames++;timing.conversionMs+=performance.now()-start; + return true; + }; const deadline = Date.now() + 6000; while (!(await draw())) { if (Date.now() > deadline) throw new DOMException('No camera frames', 'NotReadableError'); @@ -227,11 +254,19 @@ return stream; } catch (error) { await stop().catch(() => {}); throw error; } } - async function getCamera(name, config = {}) { + function getCamera(name, config = {}) { + const ticket = ++generation; + const request = opening.then(() => openCamera(name, config, ticket)); + opening = request.catch(() => {}); + return request; + } + async function openCamera(name, config, ticket) { if (config.mode && config.mode !== 'picture') throw new DOMException('Video capture is not ported yet', 'NotSupportedError'); - const ticket = ++generation; - await current?.release?.(); + if (current) await releaseStream(current); + await closing; + if (document.hidden || ticket !== generation) + throw new DOMException('Camera request superseded or hidden', 'AbortError'); const selected = devices.get(name); cameraError = null; @@ -245,7 +280,7 @@ video: { ...(selected ? { deviceId: { exact: selected } } - : { facingMode: { ideal: 'environment' } }), + : { facingMode: { ideal: name === 'front' ? 'user' : 'environment' } }), width: { ideal: 1920 }, height: { ideal: 1080 }, }, @@ -255,7 +290,7 @@ throw error; } if (document.hidden || ticket !== generation) { - stream.getTracks().forEach((t) => t.stop()); + await releaseStream(stream); const error = new DOMException('Camera hidden', 'AbortError'); recordError(error); throw error; @@ -264,7 +299,7 @@ current = stream; let preview; try { - await discover(stream); + await discover(stream, name); if (document.hidden || ticket !== generation) throw new DOMException('Camera hidden', 'AbortError'); @@ -294,7 +329,7 @@ const native = stream.vulpesCameraMetadata; const pictureSizes = native?.pictureSizes || [size]; let pictureSize = pictureSizes.reduce((a,b)=>a.width*a.height>b.width*b.height?a:b); - let focusArea = null, zoom = 1, releasing; + let focusArea = null, zoom = 1; const configuration = () => ({ mode: 'picture', previewSize: size, @@ -378,19 +413,7 @@ emit(stream, 'previewstatechange', { newState: 'started' }); }, release() { - if (releasing) return releasing; - // Detach first: visibility changes and Gaia may release the same stream - // again while its hardware worker is still shutting down. - if (current === stream) current = null; - releasing = Promise.resolve().then(async () => { - for (const video of document.querySelectorAll('video')) { - if (video.srcObject === stream) { video.pause(); video.srcObject = null; } - } - preview.remove(); - await stream.vulpesCameraClose?.(); - stream.getTracks().forEach(t => t.stop()); - }); - return releasing; + return releaseStream(stream, preview); }, async startRecording() { throw new DOMException('Video unavailable', 'NotSupportedError'); @@ -416,12 +439,7 @@ return { camera: stream, configuration: configuration() }; } catch (error) { recordError(error); - stream.getTracks().forEach((t) => t.stop()); - if (preview) { - preview.srcObject = null; - preview.remove(); - } - if (current === stream) current = null; + await releaseStream(stream, preview).catch(recordError); throw error; } } @@ -433,8 +451,7 @@ // Keep that request valid; its completion still checks visibility. if (!current) return; ++generation; - if (current?.release) current.release().catch(recordError); - else current?.getTracks().forEach((t) => t.stop()); + releaseStream(current).catch(recordError); }; addEventListener('pagehide', stop); addEventListener('visibilitychange', () => { diff --git a/host/gaia-compat.js b/host/gaia-compat.js index 5631b14a..b0c4627b 100644 --- a/host/gaia-compat.js +++ b/host/gaia-compat.js @@ -21,7 +21,7 @@ addEventListener('error', (event) => record('error', [event.message, event.filename, event.lineno]), ); - // Desktop transports Blobs directly; Android installs a JSON-compatible codec. + // Binary settings use the same persistence format on every host. const battery = Object.assign(new EventTarget(), {level:NaN, charging:false, chargingTime:Infinity, dischargingTime:Infinity}); Object.defineProperty(navigator, 'battery', {value:battery}); async function refreshBattery() { @@ -128,15 +128,16 @@ Object.defineProperty(navigator, 'mozPhoneNumberService', {value:{ normalize: number => String(number || '').normalize('NFKC').replace(/[^0-9+*#]/g, '') }}); - const settingsCodec = window.VulpesAndroidSettingsCodec; + const settingsCodec = window.VulpesSettingsCodec; async function call(method, params) { if (!navigator.vulpesRequest) return Promise.reject(new Error('VULPES_HOST_UNAVAILABLE')); - if (settingsCodec && ['settings.set', 'media.request'].includes(method)) + if (settingsCodec && (method === 'settings.set' || + (window.VulpesPlatform === 'android' && ['media.request', 'activities.finish', 'contacts.request'].includes(method)))) params = await settingsCodec.encode(params); return navigator.vulpesRequest({ version: 1, id: ++sequence, method, params }).then((reply) => { if (reply.error) throw new DOMException(reply.error.code, reply.error.code); return settingsCodec && - ['settings.get', 'settings.snapshot', 'media.request'].includes(method) + ['settings.get', 'settings.snapshot', 'media.request', 'activities.start', 'contacts.request'].includes(method) ? settingsCodec.decode(reply.result) : reply.result; }); @@ -165,6 +166,16 @@ } return r; } + let language = navigator.language; + function setLanguage(value) { + if (typeof value !== 'string' || !/^[a-z]{2,3}(?:-[a-zA-Z0-9]{2,8})*$/.test(value)) return; + const changed = language !== value; + language = value; + if (changed) dispatchEvent(new Event('languagechange')); + } + Object.defineProperty(navigator, 'language', {get:() => language}); + Object.defineProperty(navigator, 'languages', {get:() => Object.freeze([language])}); + call('locale.current').then(setLanguage).catch(() => {}); const observers = new Map(); const settings = new EventTarget(); settings.createLock = () => ({ @@ -196,6 +207,7 @@ addEventListener('vulpes-service-event', (event) => { const { type, data } = event.detail; if (type === 'settingchange') { + if (data.key === 'language.current') setLanguage(data.value); const change = { settingName: data.key, settingValue: settingsCodec ? settingsCodec.decode(data.value) : data.value, @@ -205,8 +217,17 @@ } if (type === 'callscreen-closed') document.querySelector('iframe[name="call_screen"]')?.dispatchEvent(new CustomEvent('mozbrowserclose',{bubbles:true})); if (type === 'hardware-key') dispatchEvent(new CustomEvent('softwareButtonEvent', {detail:data})); + if (type === 'activity-launch') dispatchEvent(new CustomEvent('open-app', {detail:{...data,showApp:true,onlyShowApp:false}})); if (type === 'launch') dispatchEvent(new CustomEvent('webapps-launch', { detail: data })); - if (type === 'home' || type === 'holdhome') dispatchEvent(new CustomEvent(type)); + if (type === 'screen-off') { + dispatchEvent(new CustomEvent('secure-killapps')); + window.Service?.request('lock').catch(console.error); + } + if (type === 'screen-on') window.ScreenManager?.turnScreenOn(true); + if (type === 'home' || type === 'holdhome') { + call('activities.cancel', {}).catch(console.error); + dispatchEvent(new CustomEvent(type)); + } if (type === 'activity-view') dispatchEvent(new CustomEvent('activity-view', { detail: data })); }); function application(record) { @@ -215,7 +236,7 @@ const { id, ...publicRecord } = record; const app = Object.assign(new EventTarget(), publicRecord, { installState: 'installed', - removable: false, + removable: record.removable === true, installTime: 0, updateTime: 0, downloadAvailable: false, @@ -238,6 +259,14 @@ } const mgmt = new EventTarget(); mgmt.getAll = () => request(call('apps.list').then((apps) => apps.map(application))); + mgmt.uninstall = app => request(call('apps.uninstall',{id:new URL(app.origin).hostname.split('.')[0]})); + addEventListener('vulpes-service-event',event=>{ + const {type,data}=event.detail; + if(!['apps-installed','apps-uninstalled'].includes(type))return; + const name=type==='apps-installed'?'install':'uninstall'; + const change=new Event(name);Object.defineProperty(change,'application',{value:application(data)}); + mgmt.dispatchEvent(change);if(typeof mgmt['on'+name]==='function')mgmt['on'+name](change); + }); mgmt.getNotInstalled = () => request(Promise.resolve([])); mgmt.getIcon = async (app, size, entry) => { const manifest = app.manifest.entry_points?.[entry] || app.manifest; @@ -319,10 +348,19 @@ const handlers = new Map(); navigator.mozSetMessageHandler = (name, fn) => { handlers.set(name, fn); + if (name === 'activity') call('activities.subscribe', {}).catch(console.error); if (name === 'connection') call('iac.subscribe', {}).catch(console.error); if (name === 'alarm') call('alarms.request', { operation: 'subscribe' }).catch(console.error); }; - navigator.mozHasPendingMessage = () => false; + addEventListener('vulpes-service-event', ({detail}) => { + if (detail.type !== 'activity') return; + const {id,source} = detail.data; + setTimeout(() => handlers.get('activity')?.({source, + postResult: result => call('activities.finish',{id,result}).catch(console.error), + postError: error => call('activities.finish',{id,error:String(error)}).catch(console.error) + }),0); + }); + navigator.mozHasPendingMessage = name => name === 'activity' && (new URLSearchParams(location.search).has('activity') || /\/(pick|share)\.html$/.test(location.pathname)); navigator.mozSetMessageHandlerPromise = (promise) => promise; window.VulpesCompat = { call, request, handlers }; window.MozActivity = function (source) { @@ -657,13 +695,29 @@ Object.setPrototypeOf(GaiaElement.prototype, prototype); customElements.define(name, GaiaElement); registered.add(name); - return GaiaElement; + function LegacyGaiaElement() { + return initialize(nativeCreate(name)); + } + LegacyGaiaElement.prototype = GaiaElement.prototype; + return LegacyGaiaElement; }; document.createElement = function (name, options) { const element = nativeCreate(name, options); if (name.toLowerCase() === 'iframe') wire(element); return registered.has(name) ? initialize(element) : element; }; + // v0 created callbacks ran for detached fragments too. Gaia reads component + // state immediately after parsing its templates, before attaching them. + const innerHTML = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML'); + Object.defineProperty(Element.prototype, 'innerHTML', { + ...innerHTML, + set(value) { + innerHTML.set.call(this, value); + for (const element of this.querySelectorAll('*')) { + if (registered.has(element.localName)) initialize(element); + } + }, + }); HTMLElement.prototype.createShadowRoot = function () { const host = this, root = this.attachShadow({ mode: 'open' }); diff --git a/host/gaia-restoration.js b/host/gaia-restoration.js index f4e974b6..d5d72a6f 100644 --- a/host/gaia-restoration.js +++ b/host/gaia-restoration.js @@ -84,9 +84,15 @@ if (!event.target.closest('#messages-send-button')) return; event.preventDefault(); event.stopImmediatePropagation(); // Android owns final confirmation and delivery. Do not fabricate a sent SMS. + const view = window.ConversationView; + view?.assimilateRecipients(); + const numbers = window.Navigation?.isCurrentPanel('composer') ? + view?.recipients?.numbers : view?.activeThread?.participants; + const body = window.Compose?.getText?.() || ''; + if (!numbers?.length || !body) return; + if (window.Compose.type !== 'sms') { notice(unavailable()); return; } VulpesCompat.call('platform.sms', { - number:window.ThreadUI?.recipients?.numbers?.join(';') || '', - body:window.Compose?.getText?.() || '' + number:numbers.join(';'), body }).catch(() => notice(unavailable())); }, true); } diff --git a/host/gaia-wifi.js b/host/gaia-wifi.js index a632cd95..e2d81f9d 100644 --- a/host/gaia-wifi.js +++ b/host/gaia-wifi.js @@ -23,7 +23,9 @@ manager.connection = {status, network}; manager.connectionInformation = network ? {relSignalStrength:network.relSignalStrength} : null; if (changed) emit('statuschange', manager.connection); - emit('connectioninfoupdate', manager.connectionInformation || {}); + // Gaia's signal listeners require the associated network. A scan while + // disconnected must not announce connection information for a missing AP. + if (network) emit('connectioninfoupdate', {network, ...manager.connectionInformation}); } let scanPending; function scan() { diff --git a/host/marketplace-page.js b/host/marketplace-page.js new file mode 100644 index 00000000..e2cf40e9 --- /dev/null +++ b/host/marketplace-page.js @@ -0,0 +1,14 @@ +// Runs only on our remote catalogue. It exposes no privileged API to the page. +function installMarketplacePage(win, send) { + if(win.location.origin!=='https://vulpes-os.org' || !win.location.pathname.startsWith('/marketplace/'))return; + const doc=win.document; + for(const link of doc.querySelectorAll('a[href]')) { + const url=new URL(link.href); + if(url.origin!=='https://vulpes-os.org' || url.pathname!=='/marketplace/index.php' || !/^\d{1,10}$/.test(url.searchParams.get('download')||''))continue; + link.textContent=doc.documentElement.lang==='fr'?'Installer dans Vulpes':'Install in Vulpes'; + link.addEventListener('click',event=>{ + if(!event.isTrusted || event.button!==0 || event.ctrlKey || event.metaKey || event.shiftKey)return; + event.preventDefault();send(url.href); + }); + } +} diff --git a/host/settings-codec.js b/host/settings-codec.js new file mode 100644 index 00000000..bb361377 --- /dev/null +++ b/host/settings-codec.js @@ -0,0 +1,41 @@ +// Settings persistence and Android RPC use JSON. Preserve binary Gaia values across +// both boundaries and reconstruct Blobs in the application's own JS realm. +(() => { + const marker = '__vulpesBlobV1'; + async function encode(value) { + if (value instanceof Date) return value.toJSON(); + 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' || value instanceof Blob || value instanceof Date) 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.VulpesSettingsCodec = Object.freeze({ encode, decode }); +})(); diff --git a/host/shell.js b/host/shell.js index b1e22863..2c57bc75 100644 --- a/host/shell.js +++ b/host/shell.js @@ -50,7 +50,12 @@ window.addEventListener('load', async () => { }, allFrames: true, safeForUntrustedWebProcess: true, - matches: Runtime.apps.map((app) => 'http://' + Services.io.newURI(app.origin).host + '/*'), + matches: Runtime.apps.filter(app=>!app.installed).map((app) => 'http://' + Services.io.newURI(app.origin).host + '/*'), + }); + ChromeUtils.registerWindowActor('VulpesMarketplace', { + parent:{esModuleURI:'resource://vulpes/host/actors/MarketplaceParent.sys.mjs'}, + child:{esModuleURI:'resource://vulpes/host/actors/MarketplaceChild.sys.mjs',events:{DOMContentLoaded:{capture:true}}}, + allFrames:true,safeForUntrustedWebProcess:true,matches:['https://vulpes-os.org/marketplace/*'] }); ChromeUtils.registerWindowActor('VulpesTouch', { child: { @@ -154,6 +159,7 @@ window.addEventListener('load', async () => { home.setPointerCapture(event.pointerId); holdTimer = setTimeout(() => { held = true; + Views.hideAll(); Runtime.broadcast('holdhome', {}, 'system'); }, 650); }); @@ -162,12 +168,13 @@ window.addEventListener('load', async () => { home.addEventListener('pointerup', cancel); home.addEventListener('pointercancel', cancel); home.addEventListener('click', () => { - if (!held) Runtime.broadcast('home', {}, 'system'); + if (!held) {Views.hideAll();Runtime.broadcast('home', {}, 'system');} held = false; }); window.addEventListener('keydown', (event) => { if (event.ctrlKey && event.code === 'Space') { event.preventDefault(); + Views.hideAll(); Runtime.broadcast('holdhome', {}, 'system'); } }); diff --git a/overrides/settings.gaiamobile.org/js/main.js b/overrides/settings.gaiamobile.org/js/main.js new file mode 100644 index 00000000..fe93178a --- /dev/null +++ b/overrides/settings.gaiamobile.org/js/main.js @@ -0,0 +1,238 @@ + +require.config({baseUrl:'/js',paths:{'modules':'modules','panels':'panels','shared':'../shared/js','views':'../views'},waitSeconds:0,shim:{'dsds_settings':{exports:'DsdsSettings'},'settings':{exports:'Settings'},'shared/addons/match_pattern':{exports:'MatchPattern'},'shared/airplane_mode_helper':{exports:'AirplaneModeHelper'},'shared/apn_helper':{exports:'ApnHelper'},'shared/async_storage':{exports:'asyncStorage'},'shared/bluetooth_helper':{exports:'BluetoothHelper'},'shared/device_storage/enumerate_all':{exports:'enumerateAll'},'shared/download/download_formatter':{exports:'DownloadFormatter'},'shared/download/download_store':{exports:'DownloadStore'},'shared/download/download_ui':{exports:'DownloadUI'},'shared/download/download_helper':{exports:'DownloadHelper'},'shared/fxa_iac_client':{exports:'FxAccountsIACHelper'},'shared/homescreens/homescreen_settings':{exports:'homescreenSettings'},'shared/icc_helper':{exports:'IccHelper'},'shared/keyboard_helper':{exports:'KeyboardHelper',deps:['shared/input_mgmt/input_app_list']},'shared/language_list':{exports:'LanguageList'},'shared/lazy_loader':{exports:'LazyLoader'},'shared/manifest_helper':{exports:'ManifestHelper'},'shared/mime_mapper':{exports:'MimeMapper'},'shared/mobile_operator':{exports:'MobileOperator'},'shared/omadrm/fl':{exports:'ForwardLock'},'shared/passcode_helper':{exports:'PasscodeHelper'},'shared/sanitizer':{exports:'Sanitizer'},'shared/screen_layout':{exports:'ScreenLayout'},'shared/search_provider':{exports:'SearchProvider'},'shared/settings_helper':{exports:'SettingsHelper'},'shared/settings_listener':{exports:'SettingsListener'},'shared/settings_url':{exports:'SettingsURL'},'shared/sim_settings_helper':{exports:'SimSettingsHelper'},'shared/simslot':{exports:'SIMSlot'},'shared/simslot_manager':{exports:'SIMSlotManager',deps:['shared/simslot']},'shared/stk_helper':{exports:'STKHelper'},'shared/text_normalizer':{exports:'Normalizer'},'shared/toaster':{exports:'Toaster'},'shared/tz_select':{exports:'tzSelect',deps:['shared/icc_helper']},'shared/uuid':{exports:'uuid'},'shared/findmydevice_iac_api':{exports:'wakeUpFindMyDevice'},'shared/wifi_helper':{exports:'WifiHelper'},'vendor/jszip':{exports:'JSZip'}},modules:[{name:'main'},{name:'modules/apn/apn_settings_manager',exclude:['main','modules/async_storage']},{name:'modules/dialog_service',exclude:['main']},{name:'panels/about/panel',exclude:['main']},{name:'panels/about_more_info/panel',exclude:['main','modules/bluetooth/bluetooth_context']},{name:'panels/app_permissions_detail/panel',exclude:['main']},{name:'panels/app_permissions_list/panel',exclude:['main','modules/apps_cache']},{name:'panels/app_storage/panel',exclude:['main','modules/app_storage','modules/storage_helper']},{name:'views/phone/bluetooth/panel',exclude:['main','modules/mvvm/list_view','modules/dialog_service','modules/bluetooth/bluetooth_context']},{name:'panels/browsing_privacy/panel',exclude:['main','modules/dialog_service']},{name:'panels/call_barring/panel',exclude:['main']},{name:'panels/call_barring_passcode_change/panel',exclude:['main']},{name:'panels/date_time/panel',exclude:['main','modules/date_time']},{name:'panels/developer/panel',exclude:['main','modules/dialog_service','modules/apps_cache']},{name:'panels/developer_hud/panel',exclude:['main']},{name:'panels/display/panel',exclude:['main']},{name:'panels/feedback_choose/panel',exclude:['main']},{name:'panels/feedback_send/panel',exclude:['main']},{name:'panels/findmydevice/panel',exclude:['main','modules/settings_utils']},{name:'panels/firefox_accounts/panel',exclude:['main']},{name:'panels/firefox_sync/panel',exclude:['main','modules/settings_utils']},{name:'panels/frame/panel',exclude:['main']},{name:'panels/help/panel',exclude:['main']},{name:'panels/homescreens/panel',exclude:['main','modules/apps_cache']},{name:'panels/hotspot/panel',exclude:['main','modules/dialog_service']},{name:'panels/hotspot_wifi_settings/panel',exclude:['main']},{name:'panels/keyboard/panel',exclude:['main','modules/mvvm/list_view','modules/keyboard_context']},{name:'panels/keyboard_add_layouts/panel',exclude:['main','modules/mvvm/list_view','modules/keyboard_context','shared/keyboard_helper']},{name:'panels/languages/panel',exclude:['main','shared/keyboard_helper','modules/date_time']},{name:'panels/messaging/panel',exclude:['main','modules/messaging','modules/settings_utils']},{name:'panels/messaging_details/panel',exclude:['main','modules/messaging','modules/settings_utils']},{name:'panels/operator_settings/panel',exclude:['main','dsds_settings','modules/defer','modules/state_model','modules/mvvm/list_view','modules/dialog_service','modules/customized_network_type_map','modules/mobile/supported_network_info']},{name:'panels/root/panel',exclude:['main','panels/root/low_priority_items','modules/apps_cache','modules/addon_manager','modules/storage_helper']},{name:'panels/root/low_priority_items',exclude:['main','modules/app_storage','modules/battery','modules/bluetooth/bluetooth_context','modules/media_storage','modules/sim_security','modules/wifi_context']},{name:'panels/screen_lock/panel',exclude:['main']},{name:'panels/screen_lock_passcode/panel',exclude:['main','modules/settings_utils']},{name:'panels/search/panel',exclude:['main']},{name:'panels/simcard_manager/panel',exclude:['main']},{name:'panels/simpin/panel',exclude:['main','modules/sim_security']},{name:'panels/sound/panel',exclude:['main','modules/mobile/supported_network_info']},{name:'panels/usb_storage/panel',exclude:['main','modules/media_storage']},{name:'panels/wifi/panel',exclude:['main','modules/dialog_service']},{name:'panels/wifi_auth/panel',exclude:['main']},{name:'panels/wifi_enter_certificate_nickname/panel',exclude:['main']},{name:'panels/wifi_join_hidden/panel',exclude:['main']},{name:'panels/wifi_manage_certificates/panel',exclude:['main','modules/settings_utils']},{name:'panels/wifi_manage_networks/panel',exclude:['main','modules/dialog_service']},{name:'panels/wifi_select_certificate_file/panel',exclude:['main','modules/settings_utils']},{name:'panels/wifi_status/panel',exclude:['main']},{name:'panels/wifi_wps/panel',exclude:['main']}]});define("config/require",function(){});define('modules/base/module',[],function(){'use strict';const LOG_LEVEL={NONE:0,DEBUG:1,INFO:2,WARN:3,ERROR:4,ALL:5};var _constructorMap=(function(){var _ctorMap=new Map();var _getConstructor=function(Module){return _ctorMap.get(Module)||function(){};};var _registerConstructor=function(Module,constructor){_ctorMap.set(Module,constructor||function(){});};return{getConstructor:_getConstructor,registerConstructor:_registerConstructor};})();var _emptyFunction=function(){};var _createLogger=function(name){switch(name){case'DEBUG':return function(msg){console.log(this._msgPrefix+msg);};case'INFO':return function(msg){console.info(this._msgPrefix+msg);};case'WARN':return function(msg){console.warn(this._msgPrefix+msg);};case'ERROR':return function(msg){console.error(this._msgPrefix+msg);};}};var ModulePrototype={get _msgPrefix(){return'['+this.$name+']: ';},debug:_emptyFunction,info:_emptyFunction,warn:_emptyFunction,error:_emptyFunction,throw:function(msg){throw new Error(this._msgPrefix+msg);},set _logLevel(value){Object.keys(LOG_LEVEL).forEach((name)=>{var level=LOG_LEVEL[name];if(value>=level&&value>0){this[name.toLowerCase()]=_createLogger(name);}});},super:_constructorMap.getConstructor};var _extend=function(){switch(arguments.length){case 0:break;case 1:var Module=arguments[0];for(var prop in Module.prototype){if(prop==='$name'){continue;} +var pd=Object.getOwnPropertyDescriptor(Module.prototype,prop);if(pd){Object.defineProperty(this.prototype,prop,pd);}} +break;default:Array.prototype.forEach.call(arguments,(Module)=>{_extend.call(this,Module);});break;} +return this;};function _create(constructor){if(constructor&&typeof constructor!=='function'){throw new Error('[Module]: Invalid constructor');} +var ModuleFunc=function(){};ModuleFunc.prototype=Object.create(ModulePrototype);ModuleFunc.prototype.$name=constructor&&constructor.name||'';var Module=function(){var instance=new ModuleFunc();if(constructor){constructor.apply(instance,arguments);} +return instance;};_constructorMap.registerConstructor(Module,constructor);Module.extend=_extend;Module.prototype=ModuleFunc.prototype;return Module;} +return{LOG_LEVEL:LOG_LEVEL,create:_create};});define('modules/base/dependency_graph',['require','modules/base/module'],function(require){'use strict';var Module=require('modules/base/module');var DependencyGraph=Module.create(function DependencyGraph(dpGraph){this._nodes=dpGraph?JSON.parse(JSON.stringify(dpGraph._nodes)):{};});DependencyGraph.prototype._printNodes=function dg_printNodes(){Object.keys(this._nodes).forEach((name)=>{var node=this._nodes[name];console.log('name: '+name);console.log('children: '+ +node.children.reduce((result,name)=>{return result+' '+name;},''));console.log('dependent nodes: '+ +node.dependentNodes.reduce((result,name)=>{return result+' '+name;},''));console.log('======================');});};DependencyGraph.prototype._addDependentNodes=function dg_addDependentNodes(node,dependentNodes,traversedNodes){traversedNodes=traversedNodes||[];if(traversedNodes.indexOf(node.name)>=0){traversedNodes.push(node.name);var error=traversedNodes.join(' -> ');this.throw('circular dependency detected! '+error);} +if(dependentNodes){dependentNodes.forEach((name)=>{if(node.dependentNodes.indexOf(name)<0){node.dependentNodes.push(name);}});}else{dependentNodes=Array.prototype.slice.call(node.dependentNodes);} +dependentNodes.push(node.name);traversedNodes.push(node.name);node.children.forEach((name)=>{this._addDependentNodes(this._nodes[name],dependentNodes,traversedNodes);});dependentNodes.pop();traversedNodes.pop();};DependencyGraph.prototype.addDependency=function dg_addDep(name1,name2){var node1=this._nodes[name1];var node2=this._nodes[name2];if(!node1){node1=this._nodes[name1]={name:name1,children:[],dependentNodes:[]};} +if(!node2){node2=this._nodes[name2]={name:name2,children:[],dependentNodes:[]};} +if(node1.children.indexOf(name2)<0){node1.children.push(name2);this._addDependentNodes(node1);}};DependencyGraph.prototype.getAllDependent=function dg_getAllDep(name){var node=this._nodes[name];return node&&node.dependentNodes;};return DependencyGraph;});define('modules/mvvm/observable',['require','modules/base/module','modules/base/dependency_graph'],function(require){'use strict';var Module=require('modules/base/module');var DependencyGraph=require('modules/base/dependency_graph');var OP_PREFIX=(name)=>{return'$OP_'+name;};var Observable=Module.create(function Observable(object){this._observers={};if(object){this._initWithObject(object);}});Observable.prototype._initWithObject=function o_init(object){for(var name in object){if(typeof object[name]==='function'){this[name]=object[name];}else{_defineObservableProperty(this,name,{value:object[name]});}}};Observable.prototype._notify=function o__notify(name,newValue,oldValue){var observers=this._observers[name];if(observers){observers.forEach(function(observer){observer(newValue,oldValue);});}};Observable.prototype._removeObserver=function o__removeObserver(observer,name){var observers=this._observers[name];if(observers){var index=observers.indexOf(observer);if(index>=0){observers.splice(index,1);}}};Observable.prototype.observe=function o_observe(name,observer){if(typeof observer!=='function'){return;} +(this._observers[name]=this._observers[name]||[]).push(observer);};Observable.prototype.unobserve=function o_unobserve(name,observer){if(typeof name==='function'){Object.keys(this._observers).forEach(this._removeObserver.bind(this,name));}else{if(observer){this._removeObserver(observer,name);}else if(name in this._observers){this._observers[name]=[];}}};var _dependencyGraphs=new Map();function _getDependencyGraph(modulePrototype){var dependencyGraph=_dependencyGraphs.get(modulePrototype);if(!dependencyGraph){dependencyGraph=DependencyGraph(modulePrototype._dependencyGraph);modulePrototype._dependencyGraph=dependencyGraph;_dependencyGraphs.set(modulePrototype,dependencyGraph);} +return dependencyGraph;} +function _getAllDependentValues(observable,sourceProperty){var dependentList=observable._dependencyGraph&&observable._dependencyGraph.getAllDependent(sourceProperty);if(dependentList&&dependentList.length){return dependentList.map((name)=>{return{name:name,value:observable[name]};});}else{return null;}} +function _getterTemplate(name,defaultValue){return function(){var value=this[OP_PREFIX(name)];if(typeof value==='undefined'){value=this[OP_PREFIX(name)]=defaultValue;} +return value;};} +function _setterTemplate(name){return function(value){var oldValue=this[name];if(oldValue!==value){var dependentValues=_getAllDependentValues(this,name);this[OP_PREFIX(name)]=value;this._notify(name,value,oldValue);if(dependentValues){dependentValues.forEach((obj)=>{this._notify(obj.name,this[obj.name],obj.value);});}}};} +function _defineObservablePropertyCore(object,name,options){var dependency=options&&options.dependency;if(dependency){var dependencyGraph=_getDependencyGraph(object);dependency.forEach((dependentName)=>{dependencyGraph.addDependency(name,dependentName);});} +Object.defineProperty(object,name,{enumerable:true,get:options.get,set:options.set});} +function _defineObservableProperty(object,name,options){if(options&&options.readonly){var internalName='_'+name;_defineObservablePropertyCore(object,name,{dependency:[internalName],get:function(){return this[internalName];}});_defineObservablePropertyCore(object,internalName,{get:_getterTemplate(internalName,options&&options.value),set:_setterTemplate(internalName)});}else if(options&&options.dependency&&options.dependency.length){if(typeof options.get!=='function'){throw new Error('Observable: getter of '+name+' is invalid');} +_defineObservablePropertyCore(object,name,{dependency:options.dependency,get:options.get});}else{_defineObservablePropertyCore(object,name,{get:_getterTemplate(name,options&&options.value),set:_setterTemplate(name)});}} +Object.defineProperty(Observable,'defineObservableProperty',{get:function(){return _defineObservableProperty;}});return Observable;});define('modules/base/event_emitter',['require','modules/base/module'],function(require){'use strict';var Module=require('modules/base/module');var EventEmitter=Module.create(function EventEmitter(eventNames){if(eventNames&&eventNames.length){this._eventListeners=eventNames.reduce((result,eventName)=>{result[eventName]=[];return result;},{});}else{this.throw('no valid registered events');}});EventEmitter.prototype._emitEvent=function(eventName,value){var listeners=this._eventListeners[eventName];if(!listeners){this.throw('invalid event name: '+eventName);} +this.debug('_emitEvent:'+eventName+' '+value);var eventObj={type:eventName,detail:value};listeners.forEach((listener)=>{listener.call(this,eventObj);});};EventEmitter.prototype.addEventListener=function(eventName,listener){var listeners=this._eventListeners[eventName];if(listener&&listeners){switch(typeof listener){case'function':break;case'object':if(typeof listener.handleEvent==='function'){listener=listener.handleEvent;}else{listener=null;} +break;default:listener=null;break;} +if(listener&&listeners.indexOf(listener)<0){listeners.push(listener);}}else{this.error('addEventListener: invalid listener for '+eventName);}};EventEmitter.prototype.removeEventListener=function(eventName,listener){var listeners=this._eventListeners[eventName];if(listener&&listeners){var index=-1;var type=typeof listener;switch(type){case'function':index=listeners.indexOf(listener);break;case'object':index=listeners.indexOf(listener.handleEvent);break;} +if(index>=0){listeners.splice(index,1);}}else{this.error('removeEventListener: invalid listener for '+eventName);}};return EventEmitter;});define('modules/mvvm/observable_array',['require','modules/base/module','modules/base/event_emitter','modules/mvvm/observable'],function(require){'use strict';var Module=require('modules/base/module');var EventEmitter=require('modules/base/event_emitter');var Observable=require('modules/mvvm/observable');var Events=['insert','remove','replace','reset'];var ReadOnlyMethods=['forEach','map','every','some','indexOf','lastIndexOf','reduce','reduceRight'];var ObservableArray=Module.create(function ObservableArray(array){this.super(Observable).call(this);this.super(EventEmitter).call(this,Events);this._array=array||[];this._length=this._array.length;}).extend(Observable,EventEmitter);Observable.defineObservableProperty(ObservableArray.prototype,'length',{readonly:true});Observable.defineObservableProperty(ObservableArray.prototype,'array',{readonly:true});ReadOnlyMethods.forEach(function(op){ObservableArray.prototype[op]=function(){return this._array[op].apply(this._array,arguments);};});ObservableArray.prototype.push=function(item){this._array.push(item);this._length=this._array.length;this._emitEvent('insert',{index:this._array.length-1,count:1,items:[item]});};ObservableArray.prototype.pop=function(){if(!this._array.length){return null;} +var item=this._array.pop();this._length=this._array.length;this._emitEvent('remove',{index:this._array.length,count:1,items:[item]});return item;};ObservableArray.prototype.splice=function(index,count){if(arguments.length<2){return[];} +if(index>=this._length){index=this._length-1;}else if(index<0){index=this._length+index;} +if(count<0){count=0;} +var addedItems=Array.prototype.slice.call(arguments,2);var removedItems=this._array.splice.apply(this._array,arguments);this._length=this._array.length;if(removedItems.length){this._emitEvent('remove',{index:index,count:count,items:removedItems});} +if(addedItems.length){this._emitEvent('insert',{index:index,count:addedItems.length,items:addedItems});} +return removedItems;};ObservableArray.prototype.set=function(index,value){if(index<0||index>=this._array.length){this.throw('set: out of range');} +var oldValue=this._array[index];this._array[index]=value;this._emitEvent('replace',{index:index,oldValue:oldValue,newValue:value});};ObservableArray.prototype.get=function(index){return this._array[index];};ObservableArray.prototype.reset=function(array){this._array=array||[];this._length=this._array.length;this._emitEvent('reset',{items:this._array});};return ObservableArray;});/** + * PageTransitions provides transition functions used when navigating panels. + * + * @module PageTransitions + */ +define('modules/page_transitions',[],function() { + 'use strict'; + + var _sendPanelReady = function _send_panel_ready(oldPanelHash, newPanelHash) { + var detail = { + previous: oldPanelHash, + current: newPanelHash + }; + var event = new CustomEvent('panelready', {detail: detail}); + window.dispatchEvent(event); + }; + + return { + /** + * Typically used with phone size device layouts. + * + * @alias module:PageTransitions#oneColumn + * @param {String} oldPanel + * @param {String} newPanel + * @param {Function} callback + */ + oneColumn: function pt_one_column(oldPanel, newPanel, callback) { + if (oldPanel === newPanel) { + callback(); + return; + } + + // switch previous/current classes + if (oldPanel) { + oldPanel.className = newPanel.className ? '' : 'previous'; + } + if (newPanel.className === 'current') { + _sendPanelReady(oldPanel && '#' + oldPanel.id, '#' + newPanel.id); + + if (callback) { + callback(); + } + return; + } + + // A hidden or newly inserted panel may not animate. Complete navigation + // in that case too, otherwise its onShow hook never runs. + var completed = false; + var fallback; + function paintWait(event) { + if (event && event.target !== newPanel) { + return; + } + if (completed) { + return; + } + completed = true; + clearTimeout(fallback); + newPanel.removeEventListener('transitionend', paintWait); + newPanel.removeEventListener('transitioncancel', paintWait); + _sendPanelReady(oldPanel && '#' + oldPanel.id, '#' + newPanel.id); + if (callback) { + callback(); + } + } + newPanel.addEventListener('transitionend', paintWait); + newPanel.addEventListener('transitioncancel', paintWait); + newPanel.className = 'current'; + var style = window.getComputedStyle(newPanel); + var seconds = function(value) { + return parseFloat(value) * (value.trim().endsWith('ms') ? 1 : 1000) || 0; + }; + var durations = style.transitionDuration.split(',').map(seconds); + var delays = style.transitionDelay.split(',').map(seconds); + var duration = Math.max.apply(Math, durations.map(function(value, index) { + return value + delays[index % delays.length]; + })); + fallback = setTimeout(paintWait, Math.max(0, duration) + 100); + + /** + * Most browsers now scroll content into view taking CSS transforms into + * account. That's not what we want when moving between
s, + * because the being-moved-to section is offscreen when we navigate to its + * #hash. The transitions assume the viewport is always at document 0,0. + * So add a hack here to make that assumption true again. + * https://bugzilla.mozilla.org/show_bug.cgi?id=803170 + */ + if ((window.scrollX !== 0) || (window.scrollY !== 0)) { + window.scrollTo(0, 0); + } + + }, + + /** + * Typically used with tablet size device layouts. + * + * @alias module:PageTransitions#twoColumn + * @param {String} oldPanel + * @param {String} newPanel + * @param {Function} callback + */ + twoColumn: function pt_two_column(oldPanel, newPanel, callback) { + if (oldPanel === newPanel) { + callback(); + return; + } + + if (oldPanel) { + oldPanel.className = newPanel.className ? '' : 'previous'; + newPanel.className = 'current'; + _sendPanelReady('#' + oldPanel.id, '#' + newPanel.id); + } else { + newPanel.className = 'current'; + _sendPanelReady(null, '#' + newPanel.id); + } + + if (callback) { + callback(); + } + } + }; +}); + +define('modules/panel',[],function(){'use strict';var _emptyFunc=function panel_emptyFunc(){};var Panel=function ctor_panel(options){var _initialized=false;options=options||{};options.onInit=options.onInit||_emptyFunc;options.onUninit=options.onUninit||_emptyFunc;options.onShow=options.onShow||_emptyFunc;options.onHide=options.onHide||_emptyFunc;options.onBeforeShow=options.onBeforeShow||_emptyFunc;options.onBeforeHide=options.onBeforeHide||_emptyFunc;return{get initialized(){return _initialized;},init:function(panel,initOptions){if(_initialized){return;} +_initialized=true;return options.onInit(panel,initOptions);},uninit:function(){if(!_initialized){return;} +_initialized=false;options.onUninit();},show:function(panel,showOptions){return Promise.resolve(this.init(panel,showOptions)).then(function(){return options.onShow(panel,showOptions);});},hide:function(){return options.onHide();},beforeShow:function(panel,beforeShowOptions){return Promise.resolve(this.init(panel,beforeShowOptions)).then(function(){return options.onBeforeShow(panel,beforeShowOptions);});},beforeHide:function(){return options.onBeforeHide();}};};return Panel;});define('modules/settings_cache',[],function(){'use strict';var _settings=window.navigator.mozSettings;var _settingsCache=null;var _settingsCacheRequestSent=null;var _pendingSettingsCallbacks=[];var _callbacks=[];var _onSettingsChange=function sc_onSettingsChange(event){var key=event.settingName;var value=event.settingValue;if(_settingsCache){_settingsCache[key]=value;} +_callbacks.forEach(function(callback){callback(event);});};if(_settings){_settings.onsettingchange=_onSettingsChange;} +var SettingsCache={reset:function sc_reset(){_settings=window.navigator.mozSettings;if(_settings){_settings.onsettingchange=_onSettingsChange;} +_settingsCache=null;_settingsCacheRequestSent=null;_pendingSettingsCallbacks=[];_callbacks=[];},get cache(){return _settingsCache;},getSettings:function sc_getSettings(callback){if(!_settings){return;} +if(_settingsCache&&callback){callback(_settingsCache);return;} +if(!_settingsCacheRequestSent&&!_settingsCache){_settingsCacheRequestSent=true;var lock=_settings.createLock();var request=lock.get('*');request.onsuccess=function(e){var result=request.result;var cachedResult={};for(var attr in result){cachedResult[attr]=result[attr];} +_settingsCache=cachedResult;var cbk;while((cbk=_pendingSettingsCallbacks.pop())){cbk(result);}};} +if(callback){_pendingSettingsCallbacks.push(callback);}},addEventListener:function sc_addEventListener(eventName,callback){if(eventName!=='settingsChange'){return;} +var index=_callbacks.indexOf(callback);if(index===-1){_callbacks.push(callback);}},removeEventListener:function sc_removeEventListsner(eventName,callback){if(eventName!=='settingsChange'){return;} +var index=_callbacks.indexOf(callback);if(index!==-1){_callbacks.splice(index,1);}}};SettingsCache.getSettings(null);return SettingsCache;});define('modules/panel_utils',['require','settings','modules/settings_cache','shared/lazy_loader'],function(require){'use strict';var Settings=require('settings');var SettingsCache=require('modules/settings_cache');var LazyLoader=require('shared/lazy_loader');var _settings=navigator.mozSettings;function openDialog(dialogID,onSubmit,onReset){if('#'+dialogID===Settings.currentPanel){return;} +var origin=Settings.currentPanel;Settings.currentPanel=dialogID;var dialog=document.getElementById(dialogID);var submit=dialog.querySelector('[type=submit]');if(submit){submit.onclick=function onsubmit(){if(typeof onSubmit==='function'){(onSubmit.bind(dialog))();} +Settings.currentPanel=origin;};} +var reset=dialog.querySelector('[type=reset]');if(reset){reset.onclick=function onreset(){if(typeof onReset==='function'){(onReset.bind(dialog))();} +Settings.currentPanel=origin;};}} +function openLink(url){if(url.startsWith('tel:')){var telActivity=new window.MozActivity({name:'dial',data:{type:'webtelephony/number',number:url.substr(4)}});telActivity.onsuccess=function(){};}else if(!url.startsWith('#')){var linkActivity=new window.MozActivity({name:'view',data:{type:'url',url:url}});linkActivity.onsuccess=function(){};}} +var _openDialog=function pu_openDialog(dialogID){var dialog=document.getElementById(dialogID);var fields=Array.prototype.slice.call(dialog.querySelectorAll('[data-setting]:not([data-ignore])'));var updateInput=function(lock,input){var key=input.dataset.setting;var request=lock.get(key);request.onsuccess=function(){switch(input.type){case'gaia-radio':input.checked=(input.value==request.result[key]);break;case'checkbox':case'gaia-checkbox':input.checked=request.result[key]||false;break;case'select-one':input.value=request.result[key]||'';break;default:input.value=request.result[key]||'';break;}};};function reset(){if(_settings){var lock=_settings.createLock();fields.forEach(updateInput.bind(null,lock));}} +function submit(){if(_settings){fields=Array.prototype.slice.call(dialog.querySelectorAll('[data-setting]:not([data-ignore])'));var cset={},key;var lock=_settings.createLock();fields.forEach(function(input){key=input.dataset.setting;switch(input.type){case'gaia-radio':if(input.checked){cset[key]=input.value;} +break;case'checkbox':case'gaia-checkbox':cset[key]=input.checked;break;default:cset[key]=input.value;break;}});lock.set(cset);}} +reset();openDialog(dialogID,submit);};return{activate:function pu_activate(panel){var scripts=panel.getElementsByTagName('script');var scripts_src=Array.prototype.map.call(scripts,function(script){return script.getAttribute('src');});LazyLoader.load(scripts_src);var _onclick=function(evt){if(!this.dataset.href){this.dataset.href=this.href;this.href='#';} +var href=this.dataset.href;if(!href.startsWith('#')){evt.target.blur();openLink(href);}else if(!href.endsWith('Settings')){openDialog(href.substr(1));}else{_openDialog(href.substr(1));} +return false;};var rule='a[href^="http"], a[href^="tel"], [data-href]';var links=panel.querySelectorAll(rule);var i,count;for(i=0,count=links.length;i{switch(input.type){case'gaia-switch':case'gaia-checkbox':case'checkbox':case'switch':value=!!value;if(input.checked===value){return;} +input.checked=value;break;case'range':if(input.value===value){return;} +input.value=value;break;case'select':for(i=0,count=input.options.length;i gaia-header';var header=document.querySelector(headerSelector);return(header&&header.dataset.href||'').replace('#','');};var _shallCloseActivity=function ss_shallCloseActivity(panelId){if(panelId==='close'){return true;} +if(!_currentNavigation){return false;} +var parentPanelId=_retriveParentPanelId(_currentNavigation.panelId);return _currentNavigation.panelId===_activityHandler.targetPanelId&&panelId===parentPanelId;};var _transit=function ss_transit(oldPanel,newPanel,callback){var promise=new Promise(function(resolve){var wrappedCallback=function(){if(typeof callback==='function'){callback();} +resolve();};if(_isTabletAndLandscape()){PageTransitions.twoColumn(oldPanel,newPanel,wrappedCallback);}else{PageTransitions.oneColumn(oldPanel,newPanel,wrappedCallback);}});return promise;};var _loadPanel=function ss_loadPanel(panelId,callback){var panelElement=document.getElementById(panelId);if(panelElement.dataset.rendered){callback();return;} +panelElement.dataset.rendered=true;if(panelElement.dataset.requireSubPanels){var selector='section[id^="'+panelElement.id+'-"]';var subPanels=document.querySelectorAll(selector);for(var i=0,il=subPanels.length;i1){pendingTargetPanel='call-iccs';} +SettingsService.navigate(pendingTargetPanel);break;default:if(pendingTargetPanel){SettingsService.navigate(pendingTargetPanel);} +break;}} +window.removeEventListener('panelready',onPanelReady);window.performance.mark('visuallyLoaded');document.body.dataset.ready=true;},false);window.addEventListener('telephony-settings-loaded',function onTelephonySettingsLoaded(){window.removeEventListener('telephony-settings-loaded',onTelephonySettingsLoaded);window.performance.mark('fullyLoaded');});SettingsService.init({rootPanelId:'root',context:window.LaunchContext});var options={SettingsService:SettingsService,ScreenLayout:ScreenLayout};Settings.init(options);if(navigator.mozAudioChannelManager){navigator.mozAudioChannelManager.volumeControlChannel='notification';}});require(['boot']);});define("main",function(){}); \ No newline at end of file diff --git a/overrides/shared/resources/branding/vulpes-symbol.png b/overrides/shared/resources/branding/vulpes-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..7dd01e6349d45c26c18c790e8f8d4c3bc3ea2310 GIT binary patch literal 138726 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

7al*`h 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 z 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 literal 0 HcmV?d00001 diff --git a/overrides/system.gaiamobile.org/js/system_banner.js b/overrides/system.gaiamobile.org/js/system_banner.js new file mode 100755 index 00000000..c88f6b55 --- /dev/null +++ b/overrides/system.gaiamobile.org/js/system_banner.js @@ -0,0 +1,203 @@ +/* global LazyLoader */ +'use strict'; + +(function(exports) { + + /** + * SystemBanner displays a type of notification to the user in certain cases. + * It is a type of temporary notification that does not live in the + * notifications tray. Examples of the SystemBanner implementation include + * application installation, crash reporter, and low storage notices. + * @class SystemBanner + */ + function SystemBanner() { + this._generation = 0; + } + + /** + * Amount of time before banner is automatically dismissed. + */ + SystemBanner.TIMEOUT = 4000; + + SystemBanner.prototype = { + + /** + * A reference to the SystemBanner element + * @memberof SystemBanner.prototype + * @type {Element} + */ + _banner: null, + + /** + * Set when the clicked callback called. Bypasses dismiss. + * @memberof SystemBanner.prototype + * @type {Boolean} + */ + _clicked: false, + + /** + * Set to true when the banner is currently showing to the user. + * @memberof SystemBanner.prototype + * @type {Boolean} + */ + _showing: false, + + /** + * Generates and returns the banner if it does not exist + * @memberof SystemBanner.prototype + * @type {Function} + */ + getBanner: function() { + if (this._banner) { + return Promise.resolve(this._banner); + } + + return LazyLoader.load('shared/elements/gaia-component/gaia-component.js') + .then(() => { + return LazyLoader.load('shared/elements/gaia-toast/gaia-toast.js'); + }) + .then(() => { + this._banner = document.createElement('gaia-toast'); + this._banner.timeout = SystemBanner.TIMEOUT; + this._banner.className = 'banner'; + this._banner.dataset.zIndexLevel = 'system-notification-banner'; + var container = createChild(this._banner, 'div', 'container'); + createChild(container, 'div', 'messages'); + createChild(container, 'div', 'buttons'); + return this._banner; + }); + }, + + /** + * Shows a banner with a given message. + * Optionally shows a button with a given label/callback/dismiss. + * 'dismiss' is called when the banner is dismissed and button + * has not been clicked. It is optional. + * @memberof SystemBanner.prototype + * @param {String|Array} messages The messages to display + * @param {Object} buttonParams + * { label: l10nAttrs, callback: ..., dismiss: ... } + */ + show: function(messages, buttonParams) { + var generation = ++this._generation; + clearTimeout(this._dismissTimer); + if (this._cancelRemoval) { + this._cancelRemoval(); + } + this._clicked = false; + this._showing = true; + + return this.getBanner().then(banner => { + if (generation !== this._generation || !this._showing) { + return; + } + // Clear the button and message containers. + var messageContainer = banner.querySelector('.messages'); + var buttonContainer = banner.querySelector('.buttons'); + messageContainer.innerHTML = ''; + buttonContainer.innerHTML = ''; + + // Populate banner with messages. + if (!Array.isArray(messages)) { + messages = [messages]; + } + messages.forEach((chunk) => { + var p = createChild(messageContainer, 'p'); + setElementL10n(p, chunk); + }); + + // Populate buttons if specified. + if (buttonParams) { + var button = createChild(buttonContainer, 'span'); + button.className = 'button'; + setElementL10n(button, buttonParams.label); + button.addEventListener('click', () => { + this._clicked = true; + this.hide(); + buttonParams.callback && buttonParams.callback(); + }); + } + + // Hide banner and invoke button dismiss() on click or timeout. + var hideBanner = () => { + if (generation !== this._generation || !this._showing) { + return; + } + this.hide(); + if (!this._clicked) { + buttonParams && buttonParams.dismiss && buttonParams.dismiss(); + } + }; + banner.onclick = hideBanner; + this._dismissTimer = setTimeout(hideBanner, SystemBanner.TIMEOUT); + + document.getElementById('screen').appendChild(banner); + banner.show(); + }); + }, + + /** + * Hide the System Banner and remove it from the DOM. + */ + hide: function() { + if (!this._showing) { + return; + } + this._showing = false; + clearTimeout(this._dismissTimer); + var generation = this._generation; + + return this.getBanner().then(banner => { + if (generation !== this._generation || this._showing) { + return; + } + // Animation events inside a modern shadow root do not reach the host. + var animation = banner.shadowRoot && + banner.shadowRoot.querySelector('.bread') || banner; + var cleanup = () => { + clearTimeout(this._removeTimer); + animation.removeEventListener('animationend', removeBanner); + animation.removeEventListener('animationcancel', removeBanner); + this._cancelRemoval = null; + }; + var removeBanner = event => { + if (event && event.target !== animation) { + return; + } + cleanup(); + if (!this._showing && generation === this._generation) { + banner.parentNode && banner.parentNode.removeChild(banner); + } + }; + this._cancelRemoval = cleanup; + animation.addEventListener('animationend', removeBanner); + animation.addEventListener('animationcancel', removeBanner); + // Gaia's exit animation lasts 600 ms. Also dismiss if it is disabled + // or no animation event is produced. + this._removeTimer = setTimeout(removeBanner, 700); + banner.hide(); + }); + } + }; + + function createChild(parentEl, type, className) { + var el = document.createElement(type); + if (className) { + el.className = className; + } + parentEl.appendChild(el); + return el; + } + + function setElementL10n(element, l10nAttrs) { + if (typeof(l10nAttrs) === 'string') { + element.setAttribute('data-l10n-id', l10nAttrs); + } else { + navigator.mozL10n.setAttributes( + element, l10nAttrs.id, l10nAttrs.args); + } + } + + exports.SystemBanner = SystemBanner; + +}(window)); diff --git a/release.json b/release.json index ac787205..2e7fb88a 100644 --- a/release.json +++ b/release.json @@ -1,15 +1,15 @@ { "gaia": "2.7.0", "channel": "preview", - "revision": 15, - "androidVersionCode": 15, + "revision": 17, + "androidVersionCode": 17, "geckoview": "156.0.20260921121718", "engineAlignment": { "family": "156", "patchParity": false, "reason": "Firefox desktop 156.0.1; Mozilla's GeckoView release repository currently offers 156.0 only. Keep the desktop patch and report actual engine versions." }, - "version": "2.7-preview.15", + "version": "2.7-preview.17", "adapters": { "desktop": "0.1.0-dev", "android": "0.1.0-dev" diff --git a/services/activities.mjs b/services/activities.mjs new file mode 100644 index 00000000..4072ca49 --- /dev/null +++ b/services/activities.mjs @@ -0,0 +1,114 @@ +export function isGaiaActivity(source) { + return source?.name === 'pick' || + (source?.name === 'dial' && source.data?.type === 'webtelephony/number') || + (source?.name === 'new' && source.data?.type === 'websms/sms') || + (['new','update','open'].includes(source?.name) && source.data?.type === 'webcontacts/contact'); +} + +function entryPointFor(app, path) { + if (!path) return undefined; + const pathname=new URL(path,app.origin).pathname; + return Object.entries(app.manifest?.entry_points || {}).find(([,point])=> + new URL(point.launch_path || '/',app.origin).pathname===pathname)?.[0]; +} + +// Route Gaia's built-in pickers; only the selected provider can finish a request. +export class Activities { + constructor({identity, apps, launch, notify, uuid}) { + Object.assign(this, {identity, apps, launch, notify, uuid}); + this.pending = new Map(); + this.listeners = new Map(); + } + route(source, caller) { + const types = [].concat(source?.data?.type || []), name=source?.name; + let id; + if (name === 'pick') { + if (types.some(t=>['ringtone','alerttone'].includes(t))) id='ringtones'; + else if (types.some(t=>['webcontacts/contact','webcontacts/tel','webcontacts/email','webcontacts/select'].includes(t))) id='communications'; + else if (types.some(t=>typeof t==='string' && t.startsWith('image/'))) id=caller?.id==='communications'?'gallery':'wallpaper'; + } else if (name === 'dial' && types.includes('webtelephony/number')) id='communications'; + else if (name === 'new' && types.includes('websms/sms')) id='sms'; + else if (['new','update','open'].includes(name) && types.includes('webcontacts/contact')) id='communications'; + const app = this.apps().find(a=>a.id===id); + const candidates = [].concat(app?.manifest.activities?.[name] || []); + const definition = candidates.find(d=>{ + const filter=d.filters?.type; + const accepted=[].concat(filter?.value || filter || []); + return !accepted.length || types.some(t=>accepted.includes(t)); + }); + if (!app || !definition?.href) throw Error('ACTIVITY_NOT_SUPPORTED'); + const url=new URL(definition.href,app.origin); + if (url.origin!==new URL(app.origin).origin) throw Error('ACTIVITY_NOT_SUPPORTED'); + // SMS handles the activity after startup; its old manifest fragment is + // not a Navigation view in the packaged Gaia build. + if (id==='sms' && name==='new') url.hash=''; + const entryPoint=entryPointFor(app,url.href); + return {app,definition,url,entryPoint}; + } + provider(source) { return this.route(source).app; } + start(caller, source) { + const app = this.identity(caller); + if (!app || !['settings','system','homescreen','verticalhome','ringtones','sms','communications','email','calendar'].includes(app.id)) + throw Error('PERMISSION_DENIED'); + const {app:provider,definition,url,entryPoint}=this.route(source,app); + if ([...this.pending.values()].some(p=>p.provider.id===provider.id)) throw Error('ACTIVITY_BUSY'); + const id = this.uuid(); + return new Promise((resolve,reject) => { + const row = {id,caller,app,provider,source,resolve,reject,url,entryPoint, + returnURL:caller.manager?.documentURI?.spec,returns:definition.disposition!=='window'}; + this.pending.set(id,row); + const listener=[...this.listeners.values()].find(peer=> + peer.manager?.documentURI?.spec && this.identity(peer)?.id===provider.id && this.matches(peer,row)); + if (listener) { + this.launch(provider,listener.manager.documentURI.spec,{entryPoint}); + this.deliver(row,listener); + } else { + url.searchParams.set('activity',id); + this.launch(provider,url.href,{reload:true,entryPoint}); + } + }); + } + matches(peer,row) { + const spec=peer.manager?.documentURI?.spec; + if (!spec) return true; + const current=new URL(spec); + if (current.pathname!==row.url.pathname) return false; + // Gallery selects its picker mode once at startup from #pick. Reusing a + // normal gallery window would never enable selection/cropping controls. + return row.provider.id!=='gallery' || current.hash===row.url.hash; + } + subscribe(peer) { + const app = this.identity(peer); + if (!app) throw Error('PERMISSION_DENIED'); + this.listeners.set(peer,peer); + for (const row of this.pending.values()) + if (row.provider.id===app.id && this.matches(peer,row)) this.deliver(row,peer); + } + deliver(row,peer) { + if (!peer || row.delivered === peer) return; + row.delivered = peer; + this.notify(peer,'activity',{id:row.id,source:row.source}); + if (!row.returns) {this.pending.delete(row.id);row.resolve(null);} + } + finish(peer, {id,result,error}) { + const row = this.pending.get(id); + if (!row || row.delivered !== peer) throw Error('PERMISSION_DENIED'); + this.pending.delete(id); + this.launch(row.app,row.returnURL,{entryPoint:entryPointFor(row.app,row.returnURL)}); + if (error) row.reject(Error('ACTIVITY_CANCELLED')); + else row.resolve(result); + } + cancel(peer) { + if (this.identity(peer)?.id !== 'system') throw Error('PERMISSION_DENIED'); + for (const row of this.pending.values()) row.reject(Error('ACTIVITY_CANCELLED')); + this.pending.clear(); + } + cleanup(peer) { + for (const [app, listener] of this.listeners) if (listener === peer) this.listeners.delete(app); + for (const [id,row] of this.pending) { + if (row.caller === peer || row.delivered === peer) { + this.pending.delete(id);row.reject(Error('ACTIVITY_CANCELLED')); + } + } + } +} diff --git a/services/packages.mjs b/services/packages.mjs new file mode 100644 index 00000000..7885c980 --- /dev/null +++ b/services/packages.mjs @@ -0,0 +1,77 @@ +// Archived Marketplace packages run as ordinary web apps, never certified Gaia apps. +const MAX_ZIP = 16 * 1024 * 1024, MAX_FILES = 2048, MAX_EXPANDED = 48 * 1024 * 1024; +export function packageURL(value) { + const url = new URL(value); + if (url.origin !== 'https://vulpes-os.org' || url.pathname !== '/marketplace/index.php' || + !/^\d{1,10}$/.test(url.searchParams.get('download') || '') || url.username || url.password) + throw Error('INVALID_PACKAGE_URL'); + return url.origin+url.pathname+'?download='+url.searchParams.get('download'); +} +async function bounded(stream, limit) { + const reader=stream.getReader(), chunks=[]; let size=0; + try { for (;;) {const {done,value}=await reader.read();if(done)break;size+=value.length; + if(size>limit)throw Error('PACKAGE_TOO_LARGE');chunks.push(value);} + } finally {await reader.cancel().catch(()=>{});} + const bytes=new Uint8Array(size);let offset=0;for(const chunk of chunks){bytes.set(chunk,offset);offset+=chunk.length;}return bytes; +} +function crc32(bytes) {let crc=0xffffffff;for(const byte of bytes){crc^=byte;for(let i=0;i<8;i++)crc=(crc>>>1)^((crc&1)?0xedb88320:0);}return (crc^0xffffffff)>>>0;} +function base64(bytes) {let text='';for(let i=0;iMAX_ZIP)throw Error('PACKAGE_TOO_LARGE'); + const view=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength), decoder=new TextDecoder('utf-8',{fatal:true}); + let end=-1; + for(let p=bytes.length-22;p>=Math.max(0,bytes.length-65557);p--) + if(view.getUint32(p,true)===0x06054b50 && p+22+view.getUint16(p+20,true)===bytes.length){end=p;break;} + if(end<0 || view.getUint16(end+4,true) || view.getUint16(end+6,true))throw Error('INVALID_ZIP'); + const count=view.getUint16(end+10,true), centralSize=view.getUint32(end+12,true),start=view.getUint32(end+16,true); + if(!count || count>MAX_FILES || start+centralSize!==end || view.getUint16(end+8,true)!==count)throw Error('INVALID_ZIP'); + const files=Object.create(null);let cursor=start,total=0; + for(let i=0;iend || view.getUint32(cursor,true)!==0x02014b50)throw Error('INVALID_ZIP'); + const flags=view.getUint16(cursor+8,true),method=view.getUint16(cursor+10,true),crc=view.getUint32(cursor+16,true), + packed=view.getUint32(cursor+20,true),size=view.getUint32(cursor+24,true),length=view.getUint16(cursor+28,true), + extra=view.getUint16(cursor+30,true),comment=view.getUint16(cursor+32,true),attrs=view.getUint32(cursor+38,true),offset=view.getUint32(cursor+42,true); + if(cursor+46+length+extra+comment>end || flags&1 || ![0,8].includes(method) || ((attrs>>>16)&0xf000)===0xa000)throw Error('UNSUPPORTED_ZIP'); + const name=decoder.decode(bytes.subarray(cursor+46,cursor+46+length));cursor+=46+length+extra+comment; + if(!name || name.startsWith('/') || name.includes('\\') || /[\x00-\x1f:]/.test(name) || name.split('/').some(p=>p==='..'||p==='.') || name.length>512)throw Error('INVALID_PATH'); + if(name.endsWith('/'))continue; + if(Object.hasOwn(files,name) || (total+=size)>MAX_EXPANDED || offset+30>start || view.getUint32(offset,true)!==0x04034b50)throw Error('INVALID_ZIP'); + const localName=view.getUint16(offset+26,true),localExtra=view.getUint16(offset+28,true),data=offset+30+localName+localExtra; + if(data+packed>start || decoder.decode(bytes.subarray(offset+30,offset+30+localName))!==name || view.getUint16(offset+8,true)!==method)throw Error('INVALID_ZIP'); + let content=bytes.subarray(data,data+packed); + if(method===8)content=await bounded(new Blob([content]).stream().pipeThrough(new DecompressionStream('deflate-raw')),size); + if(content.length!==size || crc32(content)!==crc)throw Error('CORRUPT_PACKAGE'); + files[name]=base64(content); + } + if(cursor!==end || !files['manifest.webapp'])throw Error('MISSING_MANIFEST'); + const original=JSON.parse(decoder.decode(Uint8Array.from(atob(files['manifest.webapp']),c=>c.charCodeAt(0)))); + if(typeof original.name!=='string' || !original.name.trim() || original.name.length>120)throw Error('INVALID_MANIFEST'); + function path(value) {if(typeof value!=='string')throw Error('INVALID_MANIFEST');const u=new URL(value,'https://app.invalid/'); + if(u.origin!=='https://app.invalid' || !Object.hasOwn(files,decodeURIComponent(u.pathname.slice(1))))throw Error('MISSING_APP_FILE');return u.pathname+u.search+u.hash;} + const launch=path(original.launch_path || '/index.html'),icons={}; + for(const [size,value] of Object.entries(original.icons || {}))if(/^\d+$/.test(size)) {try {icons[size]=path(value);}catch(_){}} + if(!Object.keys(icons).length)throw Error('MISSING_ICON'); + const hash=await crypto.subtle.digest('SHA-256',new TextEncoder().encode(source)); + const id='user-'+Array.from(new Uint8Array(hash)).map(n=>n.toString(16).padStart(2,'0')).join('').slice(0,24); + const manifest={name:original.name,description:String(original.description||'').slice(0,1000),version:String(original.version||'1.0').slice(0,60),launch_path:launch,icons,type:'web',permissions:{}}; + return {id,manifest,files,source,bytes:total,requestedPermissions:Object.keys(original.permissions||{})}; +} +export class Packages { + constructor({save,fetchPackage=(...args)=>fetch(...args),uuid=()=>crypto.randomUUID(),changed,schedule=(fn,delay)=>setTimeout(fn,delay),unschedule=id=>clearTimeout(id)}) {Object.assign(this,{save,fetchPackage,uuid,changed,schedule,unschedule});this.pending=new Map();} + async prepare(url) { + this.cancel();const controller=this.controller=new AbortController(); + const timer=this.schedule(()=>controller.abort(),60000); + try { + const source=packageURL(url),response=await this.fetchPackage(source,{redirect:'error',credentials:'omit',signal:controller.signal}); + if(!response.ok)throw Error('DOWNLOAD_FAILED'); + const item=await unpackPackage(await bounded(response.body,MAX_ZIP),source); + if(controller.signal.aborted)throw Error('DOWNLOAD_CANCELLED'); + this.pending.clear();const token=this.uuid();this.pending.set(token,{item,expires:Date.now()+300000}); + return {token,name:item.manifest.name,bytes:item.bytes,permissions:item.requestedPermissions}; + } finally {this.unschedule(timer);if(this.controller===controller)this.controller=null;} + } + cancel(){this.controller?.abort();this.pending.clear();} + async install(token) {const row=this.pending.get(token);this.pending.delete(token);if(!row || row.expires=win.innerWidth || y>=win.innerHeight) throw Error('INVALID_TEST_POINT'); + const {setTimeout}=ChromeUtils.importESModule('resource://gre/modules/Timer.sys.mjs'); + for (const type of ['mousemove','mousedown','mouseup']) { + win.synthesizeMouseEvent(type,x,y,{button:0,clickCount:1},{isAsyncEnabled:false}); + await new Promise(resolve=>setTimeout(resolve,100)); + } + return true; + } if (message.name === 'Tap') { const {x,y}=message.data, win=this.contentWindow; if (!Number.isFinite(x) || !Number.isFinite(y) || x<0 || y<0 || diff --git a/tests/activities.test.mjs b/tests/activities.test.mjs new file mode 100644 index 00000000..0caca7f8 --- /dev/null +++ b/tests/activities.test.mjs @@ -0,0 +1,83 @@ +import assert from 'node:assert/strict'; +import {Activities} from '../services/activities.mjs'; +const apps=[{id:'settings',origin:'http://settings.localhost'},{id:'wallpaper',origin:'http://wallpaper.localhost',manifest:{activities:{pick:{href:'/pick.html'}}}},{id:'ringtones',origin:'http://ringtones.localhost',manifest:{activities:{pick:{href:'/pick.html'}}}}]; +const caller={app:apps[0],manager:{documentURI:{spec:'http://settings.localhost/#display'}}}; +const picker={app:apps[1]}, stranger={app:apps[2]}; +const events=[],launches=[]; +const broker=new Activities({identity:p=>p.app,apps:()=>apps,launch:(...a)=>launches.push(a),notify:(...a)=>events.push(a),uuid:()=> 'request-1'}); +const pending=broker.start(caller,{name:'pick',data:{type:'image/*'}}); +assert.equal(events.length,0); +broker.subscribe(picker);assert.equal(events.length,1); +assert.throws(()=>broker.finish(stranger,{id:'request-1',result:{}}),/PERMISSION_DENIED/); +assert.throws(()=>broker.start(caller,{name:'pick',data:{type:'image/*'}}),/ACTIVITY_BUSY/); +broker.finish(picker,{id:'request-1',result:{blob:{__vulpesBlobV1:true,type:'image/png',base64:'YWJj'}}}); +assert.equal((await pending).blob.base64,'YWJj');assert.equal(launches.at(-1)[1],'http://settings.localhost/#display'); +const cancelled=broker.start(caller,{name:'pick',data:{type:'image/*'}}); +broker.subscribe(picker);broker.cleanup(picker);await assert.rejects(cancelled,/ACTIVITY_CANCELLED/); +assert.throws(()=>broker.start({app:{id:'untrusted'}},{name:'pick',data:{type:'image/*'}}),/PERMISSION_DENIED/); +assert.throws(()=>broker.provider({name:'pick',data:{type:'video/mp4'}}),/ACTIVITY_NOT_SUPPORTED/); +const abandoned = broker.start(caller,{name:'pick',data:{type:'image/*'}}); +assert.throws(()=>broker.cancel(stranger),/PERMISSION_DENIED/); +broker.cancel({app:{id:'system'}}); +await assert.rejects(abandoned,/ACTIVITY_CANCELLED/); +const reopened = broker.start(caller,{name:'pick',data:{type:'image/*'}}); +broker.subscribe(picker); +broker.finish(picker,{id:'request-1',result:{reopened:true}}); +assert.equal((await reopened).reopened,true); +console.log('Activity routing, result ownership, cancellation and return navigation: OK'); +const phone={id:'communications',origin:'http://communications.localhost',manifest:{activities:{ + pick:{href:'/contacts/index.html?pick',filters:{type:{value:['webcontacts/tel']}}}, + dial:{href:'/dialer/index.html#keyboard-view',filters:{type:'webtelephony/number'},disposition:'window'}, + open:[{href:'/contacts/views/details/details.html',filters:{type:'webcontacts/contact'}}, + {href:'/contacts/views/vcard_load/vcard_load.html',filters:{type:'text/vcard'}}], +}}}; +const sms={id:'sms',origin:'http://sms.localhost',manifest:{activities:{new:{href:'/index.html#/activity-new',filters:{type:'websms/sms'},returnValue:true}}}}; +apps.push(phone,sms); +const smsPeer={app:sms,manager:{documentURI:{spec:'http://sms.localhost/index.html#composer'}}}; +const phonePeer={app:phone}; +const choose=broker.start(smsPeer,{name:'pick',data:{type:'webcontacts/tel'}}); +assert.equal(new URL(launches.at(-1)[1]).searchParams.has('pick'),true); +broker.subscribe(phonePeer); +broker.finish(phonePeer,{id:'request-1',result:{number:'5550199',name:'Test'}}); +assert.equal((await choose).number,'5550199'); +assert.equal(launches.at(-1)[1],'http://sms.localhost/index.html#composer'); +const dial=broker.start(smsPeer,{name:'dial',data:{type:'webtelephony/number',number:'5550199'}}); +broker.subscribe(phonePeer);await dial; +assert.equal(broker.pending.size,0,'dial only opens the keypad, with no stale activity left behind'); +const draft=broker.start(phonePeer,{name:'new',data:{type:'websms/sms',target:'5550199'}}); +assert.equal(new URL(launches.at(-1)[1]).hash,'','activity handler chooses the composer after startup'); +broker.subscribe(smsPeer); +assert.equal(events.at(-1)[2].source.data.target,'5550199'); +broker.finish(smsPeer,{id:'request-1',result:null});await draft; +assert.equal(broker.route({name:'open',data:{type:'webcontacts/contact'}}).url.pathname,'/contacts/views/details/details.html'); +assert.throws(()=>broker.route({name:'open',data:{type:'url'}}),/ACTIVITY_NOT_SUPPORTED/); +console.log('Contacts/SMS picker, contact details, SMS draft and dialer routing: OK'); + +let serial=0; +const nested=new Activities({identity:p=>p.app,apps:()=>apps,launch:(...a)=>launches.push(a),notify:(...a)=>events.push(a),uuid:()=>String(++serial)}); +const contactsPeer={app:phone,manager:{documentURI:{spec:'http://communications.localhost/contacts/index.html'}}}; +nested.subscribe(contactsPeer); +const nestedDraft=nested.start(contactsPeer,{name:'new',data:{type:'websms/sms',target:'5550199'}}); +nested.subscribe(smsPeer); +const nestedPicker=nested.start(smsPeer,{name:'pick',data:{type:'webcontacts/tel'}}); +assert.equal(launches.at(-1)[2].reload,undefined,'reuse the address book without destroying the outer SMS activity'); +assert.equal(events.at(-1)[0],contactsPeer); +nested.finish(contactsPeer,{id:'2',result:{number:'5550199'}}); +assert.equal((await nestedPicker).number,'5550199'); +assert.equal(nested.pending.size,1); +nested.finish(smsPeer,{id:'1',result:null});await nestedDraft; +assert.equal(nested.pending.size,0); + +const gallery={id:'gallery',origin:'http://gallery.localhost',manifest:{activities:{pick:{href:'/index.html#pick',filters:{type:['image/jpeg']},returnValue:true}}}}; +apps.push(gallery); +const normalGallery={app:gallery,manager:{documentURI:{spec:'http://gallery.localhost/index.html'}}}; +nested.subscribe(normalGallery); +const photo=nested.start(contactsPeer,{name:'pick',data:{type:'image/jpeg'}}); +assert.equal(launches.at(-1)[2].reload,true,'normal Gallery must reload in picker mode'); +assert.equal(new URL(launches.at(-1)[1]).hash,'#pick'); +const galleryPicker={app:gallery,manager:{documentURI:{spec:launches.at(-1)[1]}}}; +nested.subscribe(galleryPicker); +const photoRequest=events.at(-1)[2].id; +assert.throws(()=>nested.finish(normalGallery,{id:photoRequest,result:{}}),/PERMISSION_DENIED/); +nested.finish(galleryPicker,{id:photoRequest,result:{blob:new Blob(['image'])}}); +assert.equal(await (await photo).blob.text(),'image'); diff --git a/tests/android-sms-handoff.test.mjs b/tests/android-sms-handoff.test.mjs new file mode 100644 index 00000000..7eea152d --- /dev/null +++ b/tests/android-sms-handoff.test.mjs @@ -0,0 +1,38 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import vm from 'node:vm'; +import {readFileSync} from 'node:fs'; + +function setup(composer) { + let click, assimilated=false; + const calls=[]; + const recipients={numbers:[]}; + const context={console,location:{hostname:'sms.localhost',pathname:'/index.html'}, + document:{readyState:'complete',addEventListener:(name,handler)=>{if(name==='click')click=handler;}}, + VulpesCompat:{call:async(method,params)=>{calls.push({method,params});}}, + VulpesPlatform:'android',Navigation:{isCurrentPanel:()=>composer}, + ConversationView:{recipients,activeThread:{participants:['5550123']},assimilateRecipients(){assimilated=true;recipients.numbers=['5550199'];}}, + Compose:{type:'sms',getText:()=> 'Message à confirmer'}, + }; + context.window=context; + vm.runInNewContext(readFileSync('host/gaia-restoration.js','utf8'),context); + return {calls,context,send(){ + click({target:{closest:()=>true},preventDefault(){},stopImmediatePropagation(){}}); + assert.equal(assimilated,true); + }}; +} +test('Android handoff commits the typed recipient before opening its SMS app',()=>{ + const s=setup(true);s.send(); + assert.equal(s.calls[0].method,'platform.sms'); + assert.equal(s.calls[0].params.number,'5550199'); + assert.equal(s.calls[0].params.body,'Message à confirmer'); +}); +test('Android reply uses the conversation participants and preserves the draft',()=>{ + const s=setup(false);s.send(); + assert.equal(s.calls[0].params.number,'5550123'); + assert.equal(s.context.Compose.getText(),'Message à confirmer'); +}); +test('empty Android messages are not handed off',()=>{ + const s=setup(true);s.context.Compose.getText=()=>'';s.send(); + assert.equal(s.calls.length,0); +}); diff --git a/tests/camera-lifecycle.test.mjs b/tests/camera-lifecycle.test.mjs new file mode 100644 index 00000000..a42577de --- /dev/null +++ b/tests/camera-lifecycle.test.mjs @@ -0,0 +1,152 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import vm from 'node:vm'; +import {readFileSync} from 'node:fs'; + +function deferred() { + let resolve; + const promise = new Promise(r => { resolve = r; }); + return {promise, resolve}; +} + +// Execute the shared adapter, with controllable media promises instead of a sensor. +function setup(native = false) { + const videos = [], streams = [], requests = [], events = new EventTarget(); + const nativeRequests=[]; + let hidden = false, captureGate, playGate, labels = true; + class Document extends EventTarget { get hidden() { return hidden; } } + class Video { + get srcObject() { return this.stream; } + set srcObject(value) { this.stream = value; } + constructor() { this.dataset = {}; this.style = {}; } + play() { return playGate?.promise || Promise.resolve(); } + pause() {} + remove() { const index=videos.indexOf(this); if(index!==-1)videos.splice(index, 1); } + } + const document = new Document(); + Object.assign(document, { + documentElement: {lang:'en'}, body: {append:video => videos.push(video)}, + querySelector: () => null, querySelectorAll: () => [...videos], + createElement: tag => tag === 'canvas' ? {getContext:()=>({ + createShader:()=>({}), shaderSource(){}, compileShader(){}, + getShaderParameter:()=>false, createProgram:()=>({}), + deleteShader(){}, deleteProgram(){}, getExtension:()=>null, + })} : new Video(), + }); + const navigator = {language:'en', mediaDevices: { + async enumerateDevices() { + return ['rear','selfie'].map(deviceId => ({kind:'videoinput', deviceId, + label: labels ? (deviceId === 'rear' ? 'Back camera' : 'Front camera') : ''})); + }, + async getUserMedia(constraints) { + requests.push(constraints); + const id = constraints.video.deviceId?.exact || + (constraints.video.facingMode?.ideal === 'user' ? 'selfie' : 'rear'); + const gate = captureGate; + captureGate = null; + if (gate) await gate.promise; + const track = {readyState:'live', stop() { this.readyState = 'ended'; }, + getSettings: () => ({deviceId:id, width:640, height:480})}; + const stream = Object.assign(new EventTarget(), { + getTracks: () => [track], getVideoTracks: () => [track], + }); + streams.push(stream); + return stream; + }, + }}; + const context = {document, Document, navigator, HTMLMediaElement:Video, console, performance, + location:{hostname:'camera.localhost',port:'8765'}, parent:{}, + MutationObserver:class {observe() {}}, + Event, DOMException, CustomEvent:class extends Event {constructor(type,opts){super(type);this.detail=opts.detail;}}, + setTimeout, clearTimeout, queueMicrotask, setInterval:()=>0, clearInterval(){}, + addEventListener:events.addEventListener.bind(events), dispatchEvent:events.dispatchEvent.bind(events), + VulpesCompat:{async call(method,params) { + if(method==='platform.capabilities')return {platform:native?'tundra':'desktop',camera:true}; + nativeRequests.push(params); + if(params.action==='start')return {session:42,width:640,height:480,orientation:90}; + }}, + }; + context.window = context; + vm.runInNewContext(readFileSync(new URL('../host/gaia-camera.js', import.meta.url),'utf8'), context); + return {api:navigator.mozCameras, streams, requests, videos, nativeRequests, + unlabelled:()=>{labels=false;}, + blockCapture:()=>captureGate=deferred(), blockPlay:()=>playGate=deferred(), + hide:()=>{hidden=true;events.dispatchEvent(new Event('visibilitychange'));}, + show:()=>{hidden=false;events.dispatchEvent(new Event('visibilitychange'));}, + }; +} + +test('unlabelled device identities stay stable after front/back switches', async()=>{ + const s=setup();s.unlabelled(); + for (const [name,id] of [['back','rear'],['front','selfie'],['back','rear'],['front','selfie']]) { + const {camera}=await s.api.getCamera(name); + assert.equal(camera.getVideoTracks()[0].getSettings().deviceId,id); + } + await s.streams.at(-1).release(); +}); + +test('native preview setup failure releases the sensor before returning an error', async()=>{ + const s=setup(true); + await assert.rejects(s.api.getCamera('front'),/Camera shader compilation failed/); + assert.equal(s.nativeRequests.length,2); + assert.equal(s.nativeRequests[1].action,'stop'); + assert.equal(s.nativeRequests[1].session,42); +}); + +test('switch waits for asynchronous close even after release has detached the current stream', async()=>{ + const s=setup(); + const {camera}=await s.api.getCamera('back'); + const gate=deferred();let calls=0; + camera.vulpesCameraClose=()=>{calls++;return gate.promise;}; + const release=camera.release(); + assert.equal(camera.release(),release); + const next=s.api.getCamera('front'); + await new Promise(resolve=>setImmediate(resolve)); + assert.equal(s.requests.length,1); + gate.resolve();await release; + const replacement=await next; + assert.equal(calls,1); + assert.equal(camera.getTracks()[0].readyState,'ended'); + assert.equal(s.videos.length,1,'only the active capture video is retained'); + await replacement.camera.release(); + assert.equal(s.videos.length,0); +}); + +test('failed hardware close still stops tracks and allows reopening', async()=>{ + const s=setup();const {camera}=await s.api.getCamera('back'); + camera.vulpesCameraClose=async()=>{throw Error('device disconnected');}; + await assert.rejects(camera.release(),/device disconnected/); + assert.equal(camera.getTracks()[0].readyState,'ended'); + const next=await s.api.getCamera('front'); + assert.equal(next.camera.getTracks()[0].readyState,'live'); + await next.camera.release(); +}); + +test('concurrent switch waits for a pending open and disposes its stale stream', async()=>{ + const s=setup(),gate=s.blockCapture(); + const first=s.api.getCamera('back'); + const rejected=assert.rejects(first,{name:'AbortError'}); + await new Promise(resolve=>setImmediate(resolve)); + const second=s.api.getCamera('front'); + await new Promise(resolve=>setImmediate(resolve)); + assert.equal(s.requests.length,1,'second sensor must not open before first settles'); + gate.resolve();await rejected; + const {camera}=await second; + assert.equal(s.streams[0].getTracks()[0].readyState,'ended'); + assert.equal(camera.getVideoTracks()[0].getSettings().deviceId,'selfie'); + await camera.release(); +}); + +test('hide/show during preview startup leaves no live abandoned stream', async()=>{ + const s=setup(),gate=s.blockPlay(); + const first=s.api.getCamera('back'); + const rejected=assert.rejects(first,{name:'AbortError'}); + await new Promise(resolve=>setImmediate(resolve)); + s.hide();s.show(); + const next=s.api.getCamera('front'); + gate.resolve();await rejected; + const {camera}=await next; + assert.equal(s.streams[0].getTracks()[0].readyState,'ended'); + assert.equal(camera.getTracks()[0].readyState,'live'); + await camera.release(); +}); diff --git a/tests/contacts.test.mjs b/tests/contacts.test.mjs new file mode 100644 index 00000000..fca77a98 --- /dev/null +++ b/tests/contacts.test.mjs @@ -0,0 +1,59 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {Contacts} from '../host/Contacts.sys.mjs'; + +let serial=0; +globalThis.Services={uuid:{generateUUID:()=>({toString:()=>`test-${++serial}`})}}; +const writer={manifest:{permissions:{contacts:{access:'readwrite'}}}}; +const reader={manifest:{permissions:{contacts:{access:'readonly'}}}}; + +async function addressBook() { + let stored; + const events=[]; + const storage={async run(_store,operation,args) { + if(operation==='get') return {value:structuredClone(stored)}; + stored=structuredClone(args[0]); + }}; + const contacts=new Contacts(storage,event=>events.push(event)); + await contacts.init(); + return {contacts,events,storage}; +} + +test('contact edits, photo, date and favourite survive reopening the store',async()=>{ + const {contacts,events,storage}=await addressBook(); + const first=await contacts.request(writer,{operation:'save',contact:{ + givenName:['Élodie'],familyName:['Test'],tel:[{value:'+33 6 00 00 00 00'}], + photo:[new Blob(['portrait'],{type:'image/jpeg'})],bday:new Date('1995-08-19T00:00:00Z'), + }}); + const edited=await contacts.request(writer,{operation:'save',contact:{...first,familyName:['Modifiée'],category:['favorite']}}); + assert.equal(edited.id,first.id); + assert.equal(edited.published,first.published); + const reopened=new Contacts(storage,()=>{});await reopened.init(); + const [row]=await reopened.request(reader,{operation:'find',options:{filterBy:['id'],filterOp:'equals',filterValue:first.id}}); + assert.deepEqual(row.familyName,['Modifiée']); + assert.deepEqual(row.category,['favorite']); + assert.equal(await row.photo[0].text(),'portrait'); + assert.equal(row.bday.toISOString(),'1995-08-19T00:00:00.000Z'); + assert.deepEqual(events.map(e=>e.reason),['create','update']); + await contacts.request(writer,{operation:'remove',id:first.id}); + assert.equal(await contacts.request(reader,{operation:'count'}),0); +}); + +test('a failed write emits no contactchange and does not poison later saves',async()=>{ + const {contacts,events,storage}=await addressBook(); + const run=storage.run; + storage.run=async()=>{throw Error('disk full');}; + await assert.rejects(contacts.request(writer,{operation:'save',contact:{name:['First']}}),/disk full/); + assert.equal(events.length,0); + assert.equal(await contacts.request(reader,{operation:'count'}),0); + storage.run=run; + await contacts.request(writer,{operation:'save',contact:{name:['Second']}}); + assert.equal(await contacts.request(reader,{operation:'count'}),1); +}); + +test('read-only callers cannot change the address book',async()=>{ + const {contacts}=await addressBook(); + for(const operation of ['save','remove','clear']) + await assert.rejects(contacts.request(reader,{operation,contact:{name:['Denied']},id:'anything'}),/PERMISSION_DENIED/); + await assert.rejects(contacts.request({manifest:{}},{operation:'find'}),/PERMISSION_DENIED/); +}); diff --git a/tests/installed_apps_test.py b/tests/installed_apps_test.py new file mode 100644 index 00000000..f85116dd --- /dev/null +++ b/tests/installed_apps_test.py @@ -0,0 +1,28 @@ +import base64 +import json +from pathlib import Path +import sys +import tempfile +import unittest +sys.path.insert(0, str(Path(__file__).resolve().parents[1]/'tools')) +from installed_apps import InstalledApps + +class StoreTests(unittest.TestCase): + def test_persistence_removal_and_permission_boundary(self): + with tempfile.TemporaryDirectory() as folder: + store=InstalledApps(folder,8765) + item=dict(id='user-'+'1'*24,manifest=dict(name='Test',type='certified',permissions={'settings':{}}),files={'index.html':base64.b64encode(b'hello').decode()}) + store.save(item) + restored=InstalledApps(folder,8765) + self.assertEqual(restored.read(item['id'],'index.html'),b'hello') + self.assertEqual(restored.records()[0]['manifest']['permissions'],{}) + self.assertEqual(restored.records()[0]['manifest']['type'],'web') + self.assertEqual(json.loads(restored.read(item['id'],'manifest.webapp'))['permissions'],{}) + self.assertEqual((Path(folder)/'.token').stat().st_mode & 0o777,0o600) + self.assertIsNone(restored.read(item['id'],'../.token')) + with self.assertRaises(ValueError):restored.remove('system') + with self.assertRaises(ValueError):restored.save({**item,'id':'../escape'}) + restored.remove(item['id']) + self.assertEqual(InstalledApps(folder,8765).records(),[]) + +if __name__=='__main__':unittest.main() diff --git a/tests/packages.test.mjs b/tests/packages.test.mjs new file mode 100644 index 00000000..b5652611 --- /dev/null +++ b/tests/packages.test.mjs @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import {execFileSync} from 'node:child_process'; +import {unpackPackage,packageURL,Packages} from '../services/packages.mjs'; +const source='https://vulpes-os.org/marketplace/index.php?download=42'; +function zip(extra={},manifest={}) { + return new Uint8Array(execFileSync('python3',['-c',`import sys,io,zipfile,json +b=io.BytesIO();args=json.loads(sys.argv[1]) +with zipfile.ZipFile(b,'w',zipfile.ZIP_DEFLATED) as z: + z.writestr('manifest.webapp',json.dumps(dict(name='Test',launch_path='/index.html',icons={'128':'/icon.png'},**args['manifest']))) + z.writestr('index.html','

Test

');z.writestr('icon.png',b'icon') + for name,value in args['extra'].items():z.writestr(name,value) +sys.stdout.buffer.write(b.getvalue())`,JSON.stringify({extra,manifest})])); +} +assert.equal(packageURL(source+'&lang=fr'),source); +for(const url of ['http://vulpes-os.org/marketplace/index.php?download=42','https://evil.test/marketplace/index.php?download=42','https://vulpes-os.org/x?download=42','https://vulpes-os.org/marketplace/index.php?download=../42'])assert.throws(()=>packageURL(url)); +const bytes=zip({}, {type:'certified',permissions:{settings:{access:'readwrite'},'webapps-manage':{}}}); +const item=await unpackPackage(bytes,source); +assert.equal(item.manifest.type,'web');assert.deepEqual(item.manifest.permissions,{});assert.match(item.id,/^user-[a-f0-9]{24}$/); +for(const name of ['../escape','/absolute','dir/../../escape','dir\\escape','a:b'])await assert.rejects(unpackPackage(zip({[name]:'bad'}),source)); +await assert.rejects(unpackPackage(bytes.slice(0,-2),source)); +const corrupt=bytes.slice();corrupt[100]^=255;await assert.rejects(unpackPackage(corrupt,source)); +let saved=null,changes=0; +const service=new Packages({save:async value=>saved=value,changed:async()=>changes++,uuid:()=> 'token',fetchPackage:async()=>new Response(bytes)}); +const prepared=await service.prepare(source);assert.equal(saved,null);assert.equal(prepared.name,'Test'); +assert.equal(await service.install(prepared.token),item.id);assert.equal(changes,1);assert.equal(saved.id,item.id); +await assert.rejects(service.install(prepared.token),/INSTALL_EXPIRED/); +console.log('Package URL policy, ZIP boundaries/CRC, permission stripping and one-use confirmation: OK'); + +// Cancellation during download must not produce a confirmation token. +let finishDownload; +const cancelled=new Packages({save:async()=>assert.fail('unexpected save'),changed:async()=>{}, + fetchPackage:()=>new Promise(resolve=>{finishDownload=resolve;})}); +const pending=cancelled.prepare(source);cancelled.cancel();finishDownload(new Response(bytes)); +await assert.rejects(pending,/DOWNLOAD_CANCELLED/);assert.equal(cancelled.pending.size,0); +console.log('Cancelled download cannot be installed: OK'); diff --git a/tests/settings-codec.test.mjs b/tests/settings-codec.test.mjs new file mode 100644 index 00000000..970495b0 --- /dev/null +++ b/tests/settings-codec.test.mjs @@ -0,0 +1,35 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import vm from 'node:vm'; +import {readFileSync} from 'node:fs'; + +const context = {window:{}, Blob, Date, btoa, atob, Uint8Array}; +vm.runInNewContext(readFileSync(new URL('../host/settings-codec.js',import.meta.url),'utf8'),context); +const codec=context.window.VulpesSettingsCodec; +test('native media replies retain the structured-cloned Blob and date', async()=>{ + const blob=new Blob(['jpeg bytes'],{type:'image/jpeg'}), date=new Date(); + const result=codec.decode({blob,lastModified:date}); + assert.equal(result.blob,blob); + assert.equal(result.lastModified,date); + assert.equal(await result.blob.text(),'jpeg bytes'); +}); +test('Android JSON media and settings still round-trip binary content',async()=>{ + const source=new Blob([new Uint8Array([0,128,255,10])],{type:'image/png'}); + const wire=JSON.parse(JSON.stringify(await codec.encode({photo:source}))); + const result=codec.decode(wire); + assert.equal(result.photo.type,'image/png'); + assert.deepEqual(new Uint8Array(await result.photo.arrayBuffer()),new Uint8Array([0,128,255,10])); +}); +test('a near-match marker is ordinary data, not an injected Blob',()=>{ + const result=codec.decode({__vulpesBlobV1:true,type:'image/png',base64:'AA==',other:1}); + assert.equal(result.other,1); + assert.equal(result instanceof Blob,false); +}); + +test('Android contact birthdays keep their ISO date instead of becoming empty objects',async()=>{ + const date=new Date('1995-08-19T00:00:00.000Z'); + const wire=await codec.encode({bday:date,photo:[new Blob(['portrait'],{type:'image/jpeg'})]}); + const result=codec.decode(JSON.parse(JSON.stringify(wire))); + assert.equal(result.bday,date.toISOString()); + assert.equal(await result.photo[0].text(),'portrait'); +}); diff --git a/tests/settings-transitions.test.mjs b/tests/settings-transitions.test.mjs new file mode 100644 index 00000000..dbd37cca --- /dev/null +++ b/tests/settings-transitions.test.mjs @@ -0,0 +1,29 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import vm from 'node:vm'; +import {readFile} from 'node:fs/promises'; +const source=await readFile(new URL('../gaia/apps/settings/js/modules/page_transitions.js',import.meta.url),'utf8'); +function fixture(duration='0.4s') { + let module, timer, delay, calls=0, ready=0; + const panel=Object.assign(new EventTarget(),{id:'wifi',className:''}); + vm.runInNewContext(source,{define:fn=>{module=fn();},CustomEvent, + window:{scrollX:0,scrollY:0,dispatchEvent:()=>ready++,getComputedStyle:()=>({transitionDuration:duration,transitionDelay:'0s'})}, + setTimeout:(cb,ms)=>{timer=cb;delay=ms;return 1;},clearTimeout:()=>{}}); + module.oneColumn({id:'root',className:'current'},panel,()=>calls++); + return {panel,finish:()=>timer(),state:()=>({calls,ready,delay})}; +} +test('Settings reaches onShow when no transition event arrives',()=>{ + const f=fixture();assert.equal(f.state().calls,0);f.finish(); + assert.deepEqual(f.state(),{calls:1,ready:1,delay:500}); + f.panel.dispatchEvent(new Event('transitionend'));f.finish(); + assert.equal(f.state().calls,1); +}); +test('normal and cancelled transitions finish exactly once',()=>{ + for(const type of ['transitionend','transitioncancel']){ + const f=fixture();f.panel.dispatchEvent(new Event(type));f.finish(); + assert.equal(f.state().calls,1);assert.equal(f.state().ready,1); + } +}); +test('disabled animations do not leave navigation pending',()=>{ + const f=fixture('0s');assert.equal(f.state().delay,100);f.finish();assert.equal(f.state().calls,1); +}); diff --git a/tests/wifi-throttle.test.mjs b/tests/wifi-throttle.test.mjs index fb478211..1d155965 100644 --- a/tests/wifi-throttle.test.mjs +++ b/tests/wifi-throttle.test.mjs @@ -107,3 +107,27 @@ test('Native Wi-Fi acknowledges unchanged settings and completes delayed radio c assert.equal(enabled,1); assert.equal(navigator.mozWifiManager.enabled,true); }); + +test('scan without an association does not send Gaia an invalid signal event', async () => { + const {readFile}=await import('node:fs/promises'); + const vm=await import('node:vm'); + let connected=false, signals=0; + const navigator={}; + const context=vm.createContext({navigator,console,Event,EventTarget,DOMException, + window:{VulpesNativeWifi:true},location:{hostname:'settings.localhost'},addEventListener(){}, + VulpesCompat:{request:p=>p,call:async()=>({enabled:true,networks:[ + {ssid:'Test network',id:'/ap/1',signal:80,security:'wpa-psk',connected} + ]})}}); + vm.runInContext(await readFile(new URL('../host/gaia-wifi.js',import.meta.url),'utf8'),context); + const manager=navigator.mozWifiManager; + manager.onconnectioninfoupdate=event=>{ + // The original Settings WifiUtils reads event.network.ssid unconditionally. + assert.equal(event.network.ssid,'Test network'); + assert.equal(event.relSignalStrength,80);signals++; + }; + assert.equal((await manager.getNetworks()).length,1); + assert.equal(signals,0);assert.equal(manager.connectionInformation,null); + connected=true;await manager.getNetworks(); + assert.equal(signals,1);assert.equal(manager.connection.status,'connected'); + connected=false;await manager.getNetworks();assert.equal(signals,1); +}); diff --git a/tools/check-parity.py b/tools/check-parity.py index 7730afec..916a4819 100644 --- a/tools/check-parity.py +++ b/tools/check-parity.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 """Reject stale Android payloads and inconsistent common release metadata.""" -import argparse, hashlib, io, json, re, zipfile +import argparse, contextlib, hashlib, io, json, re, zipfile from project import ROOT, release_info p = argparse.ArgumentParser() -p.add_argument("--apk", type=str) +group = p.add_mutually_exclusive_group() +group.add_argument("--apk", type=str) +group.add_argument("--assets", action="store_true", help="Check staged Android resources, not a compiled APK") args = p.parse_args() r = release_info() apk = ROOT / (args.apk or f'android/dist/vulpes-os-preview-{r["version"]}.apk') @@ -18,8 +20,15 @@ def translate(s): return pattern.sub(lambda m: f"http://{m[1] or m[2]}.localhost:18765", s) +class StagedAssets: + def read(self, name): + if not name.startswith('assets/') or '..' in name.split('/'): + raise ValueError('Invalid packaged path') + return (ROOT / 'android/app/src/main' / name).read_bytes() + results = {} -with zipfile.ZipFile(apk) as archive: +source = contextlib.nullcontext(StagedAssets()) if args.assets else zipfile.ZipFile(apk) +with source as archive: metadata = json.loads(archive.read("assets/build-info.json")) packaged = json.loads(archive.read("assets/bridge/release.json")) assert metadata["version"] == r["version"], metadata @@ -30,6 +39,7 @@ with zipfile.ZipFile(apk) as archive: "platform-ui.js", "gaia-camera.js", "gaia-compat.js", + "settings-codec.js", "gaia-services.js", "gaia-styles.js", "gaia-restoration.js", @@ -65,6 +75,9 @@ with zipfile.ZipFile(apk) as archive: ("calendar", "js/bundle.js", "overrides"), ("gallery", "js/metadata_scripts.js", "assets/webapps"), ("gallery", "js/ImageEditor.js", "assets/webapps"), + ("communications", "contacts/js/views/form.js", "assets/webapps"), + ("communications", "contacts/js/contacts.js", "assets/webapps"), + ("communications", "contacts/js/activities.js", "assets/webapps"), ]: expected = translate((ROOT / source / (app + ".gaiamobile.org") / path).read_text()) for old, new in [("-moz-calc(", "calc("), ("-moz-box-sizing:", "box-sizing:"), @@ -76,13 +89,17 @@ with zipfile.ZipFile(apk) as archive: actual = gaia.read(app + "/" + path) assert actual == expected, "Stale application workflow: " + app + "/" + path results[app + "/" + path] = hashlib.sha256(actual).hexdigest() + for name in ("marketplace.js", "marketplace.css", "icon.png"): + expected = (ROOT / "assets/webapps/marketplace.gaiamobile.org" / name).read_bytes() + if name.endswith(".js"): expected = translate(expected.decode()).encode() + assert gaia.read("marketplace/" + name) == expected, "Stale Marketplace: " + name for path in (ROOT / "overrides/search.gaiamobile.org/home").rglob("*"): if path.is_file(): relative = path.relative_to(ROOT / "overrides/search.gaiamobile.org") assert gaia.read("search/" + relative.as_posix()) == path.read_bytes(), str(relative) assert b"vulpes-local-home" in gaia.read("search/newtab.html") assert archive.read("assets/bridge/Messages.mjs") == (ROOT / "host/Messages.sys.mjs").read_bytes() - for name in ["platform.mjs", "settings.mjs", "datastores.mjs", "screen-power.mjs"]: + for name in ["platform.mjs", "settings.mjs", "datastores.mjs", "screen-power.mjs", "activities.mjs", "packages.mjs"]: assert ( archive.read("assets/bridge/services/" + name) == (ROOT / "services" / name).read_bytes() @@ -99,8 +116,11 @@ report = { "androidEngine": r["androidEngine"], "exactEngineMatch": r["desktopEngine"] == r["androidEngine"], "sharedFiles": results, - "apk": str(apk), + "apk": None if args.assets else str(apk), + "stagedAssetsOnly": args.assets, "scope": "Payload and metadata parity; hardware behavior requires separate integration tests.", } -(ROOT / "logs/parity-payload.json").write_text(json.dumps(report, indent=2) + "\n") +report_path = ROOT / ("logs/parity-staged.json" if args.assets else "logs/parity-payload.json") +report_path.parent.mkdir(parents=True, exist_ok=True) +report_path.write_text(json.dumps(report, indent=2) + "\n") print(json.dumps(report, indent=2)) diff --git a/tools/installed_apps.py b/tools/installed_apps.py new file mode 100644 index 00000000..44dae493 --- /dev/null +++ b/tools/installed_apps.py @@ -0,0 +1,72 @@ +"""Private ordinary-web-app store. No Gaia permissions are read from packages.""" +import base64 +import json +import os +from pathlib import Path +import re +import secrets +import threading + +class InstalledApps: + def __init__(self, directory, port): + self.root = Path(directory) + self.root.mkdir(parents=True, exist_ok=True) + self.port = port + self.lock = threading.RLock() + self.token = secrets.token_hex(32) + token = self.root / '.token' + token.write_text(self.token) + token.chmod(0o600) + self.items = {} + for path in self.root.glob('user-*.json'): + item = json.loads(path.read_text()) + if self.valid_id(item.get('id')): + self.items[item['id']] = item + + @staticmethod + def valid_id(value): + return isinstance(value, str) and re.fullmatch(r'user-[a-f0-9]{24}', value) + + def records(self): + with self.lock: + return [self.record(item) for item in self.items.values()] + + def record(self, item): + origin = f'http://{item["id"]}.localhost:{self.port}' + manifest = {**item['manifest'], 'permissions': {}, 'type': 'web'} + return dict(id=item['id'], origin=origin, manifestURL=origin+'/manifest.webapp', + manifest=manifest, removable=True, installed=True) + + def save(self, item): + if not self.valid_id(item.get('id')) or not isinstance(item.get('files'), dict): + raise ValueError('INVALID_PACKAGE') + # A second boundary check keeps the storage API independent of ZIP parsing. + for name, content in item['files'].items(): + if not isinstance(name, str) or not isinstance(content, str): + raise ValueError('INVALID_PACKAGE') + with self.lock: + target = self.root / (item['id']+'.json') + stage = target.with_suffix('.tmp') + with stage.open('w') as stream: + json.dump(item, stream) + stream.flush() + os.fsync(stream.fileno()) + stage.replace(target) + self.items[item['id']] = item + + def remove(self, app_id): + if not self.valid_id(app_id): + raise ValueError('NOT_REMOVABLE') + with self.lock: + (self.root/(app_id+'.json')).unlink(missing_ok=True) + self.items.pop(app_id, None) + + def read(self, app_id, path): + with self.lock: + item = self.items.get(app_id) + if not item: + return None + if path == 'manifest.webapp': + return json.dumps(self.record(item)['manifest']).encode() + content = item['files'].get(path) + return base64.b64decode(content, validate=True) if content is not None else None diff --git a/tools/project.py b/tools/project.py index 1e8f25b7..92f5c7fd 100644 --- a/tools/project.py +++ b/tools/project.py @@ -65,3 +65,12 @@ def release_info(): if not release["androidVersionCode"] > 4: raise ValueError("Android versionCode must preserve upgrades from Preview 4") return release + + +def application_manifest(app_id, manifest, include_development=False): + """Keep development providers installed but hide their launcher entries.""" + # Runtime exports omit dev_apps sources; keep the same policy in every package. + development = app_id in ('contacts-ds-provider1', 'contacts-ds-provider2', 'contacts-manager', 'ds-test', 'geoloc', 'in_app_pay_test', 'l20n-app', 'marketplace-dev.allizom.org', 'marketplace.allizom.org', 'membuster', 'mobile-wallet', 'mochitest', 'music-oga', 'nfc-api-test', 'share-receiver', 'template', 'test-agent', 'test-container', 'test-iac-publisher', 'test-iac-subscriber', 'test-ime', 'test-keyboard-app', 'test-otasp', 'test-receiver-1', 'test-receiver-2', 'test-receiver-inline', 'test-sensors', 'test-wappush', 'testpermission', 'theme-test-1', 'theme-test-2', 'theme-test-3', 'uitest', 'uitest-privileged', 'upnp-test', 'test-startup-limit') + if development and not include_development: + manifest = {**manifest, "role": "system"} + return manifest diff --git a/tools/refresh-settings-panels.py b/tools/refresh-settings-panels.py index a677e376..ae19c69c 100644 --- a/tools/refresh-settings-panels.py +++ b/tools/refresh-settings-panels.py @@ -3,15 +3,23 @@ from pathlib import Path import re ROOT=Path(__file__).resolve().parents[1] -for panel, module in [('display','slider_handler'),('wifi','wifi_network_list')]: - name=f'panels/{panel}/{module}' - relative=f'js/panels/{panel}/panel.js' +modules = [ + ('panels/display/slider_handler', 'js/panels/display/panel.js'), + ('panels/wifi/wifi_network_list', 'js/panels/wifi/panel.js'), + ('modules/page_transitions', 'js/main.js'), +] +for name, relative in modules: original=ROOT/'assets/webapps/settings.gaiamobile.org'/relative source=ROOT/'overrides/settings.gaiamobile.org'/f'js/{name}.js' + if not source.exists(): + source=ROOT/'gaia/apps/settings'/f'js/{name}.js' text=source.read_text() dependencies=list(dict.fromkeys(re.findall(r"require\(['\"]([^'\"]+)['\"]\)",text))) import json - text=text.replace('define(function(require)', 'define('+repr(name)+','+json.dumps(['require',*dependencies])+',function(require)',1) + if 'define(function(require)' in text: + text=text.replace('define(function(require)', 'define('+repr(name)+','+json.dumps(['require',*dependencies])+',function(require)',1) + else: + text=text.replace('define(function()', 'define('+repr(name)+',[],function()',1) bundle=original.read_text() start=bundle.index("define('"+name+"'") end=bundle.find("define('",start+8) diff --git a/tools/serve-gaia.py b/tools/serve-gaia.py index 561b6135..03bf60c1 100644 --- a/tools/serve-gaia.py +++ b/tools/serve-gaia.py @@ -15,7 +15,9 @@ parser = argparse.ArgumentParser() parser.add_argument("--port", type=int, default=8765) parser.add_argument("--legacy-apps", action="store_true") args = parser.parse_args() -from project import build_info, validate_assets, release_info +from project import build_info, validate_assets, release_info, application_manifest +from installed_apps import InstalledApps +INSTALLED = InstalledApps(ROOT / "profiles/installed-apps", args.port) validate_assets() info = build_info() @@ -44,7 +46,7 @@ for manifest_path in manifests: if not re.fullmatch("[a-z0-9_-]+", name): continue origin = f"http://{name}.localhost:{args.port}" - manifest = json.loads(translate(manifest_path.read_text())) + manifest = application_manifest(name, json.loads(translate(manifest_path.read_text())), os.environ.get("VULPES_SHOW_DEV_APPS") == "1") ORIGINS[urlsplit(origin).netloc] = manifest_path.parent.resolve() REGISTRY.append( { @@ -56,7 +58,7 @@ for manifest_path in manifests: ) ORIGINS[f"theme.localhost:{args.port}"] = ORIGINS[f"default_theme.localhost:{args.port}"] (ROOT / "assets/registry.json").write_text( - json.dumps(REGISTRY, ensure_ascii=False, indent=2) + "\n" + json.dumps(REGISTRY + INSTALLED.records(), ensure_ascii=False, indent=2) + "\n" ) (ROOT / "assets/settings-current-defaults.json").write_text( translate((ROOT / "assets/settings-defaults.json").read_text()) @@ -64,7 +66,45 @@ ORIGINS[f"theme.localhost:{args.port}"] = ORIGINS[f"default_theme.localhost:{arg class Handler(BaseHTTPRequestHandler): + def json_reply(self, value): + data = json.dumps(value).encode() + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.send_header('Content-Length', str(len(data))) + self.end_headers() + self.wfile.write(data) + + def do_POST(self): + import secrets + if self.headers.get('Host') != f'system.localhost:{args.port}' or not secrets.compare_digest(self.headers.get('X-Vulpes-Install', ''), INSTALLED.token): + self.send_error(403); return + try: + length = int(self.headers.get('Content-Length', '0')) + if not 0 < length <= 70*1024*1024: raise ValueError('INVALID_SIZE') + data = json.loads(self.rfile.read(length)) + if self.path == '/_vulpes/packages/save': INSTALLED.save(data) + elif self.path == '/_vulpes/packages/remove': INSTALLED.remove(data['id']) + else: self.send_error(404); return + self.json_reply(INSTALLED.records()) + except (ValueError, KeyError, OSError): self.send_error(400) + def do_GET(self): + host = self.headers.get('Host', '').lower() + if host == f'system.localhost:{args.port}' and self.path == '/_vulpes/packages/list': + self.json_reply(INSTALLED.records()); return + app_id = host.removesuffix(f'.localhost:{args.port}') + if INSTALLED.valid_id(app_id) and host == f'{app_id}.localhost:{args.port}': + path = unquote(urlsplit(self.path).path).lstrip('/') or 'index.html' + data = INSTALLED.read(app_id, path) + if data is None: self.send_error(404); return + self.send_response(200) + self.send_header('Content-Type', mimetypes.guess_type(path)[0] or 'application/octet-stream') + self.send_header('Content-Length', str(len(data))) + self.send_header('Cache-Control', 'no-store') + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('X-Content-Type-Options', 'nosniff') + self.send_header('Content-Security-Policy', "object-src 'none'; frame-ancestors http://*.localhost:"+str(args.port)) + self.end_headers(); self.wfile.write(data); return root = ORIGINS.get(self.headers.get("Host", "").lower()) if root is None: self.send_error(421, "Unregistered application host") @@ -93,7 +133,7 @@ class Handler(BaseHTTPRequestHandler): # Only this non-privileged compatibility layer is exposed, never host modules. if path in [ "/_vulpes/" + name - for name in ["gaia-compat.js", "gaia-input.js", "gaia-services.js", "gaia-styles.js", "gaia-restoration.js", "gaia-wifi.js", "desktop.css", "platform-ui.js", "gaia-camera.js"] + for name in ["settings-codec.js", "gaia-compat.js", "gaia-input.js", "gaia-services.js", "gaia-styles.js", "gaia-restoration.js", "gaia-wifi.js", "desktop.css", "platform-ui.js", "gaia-camera.js"] ]: file = ROOT / "host" / path.rsplit("/", 1)[1] else: @@ -157,13 +197,13 @@ class Handler(BaseHTTPRequestHandler): if file.suffix == ".html" and root in LEGACY_ROOTS: data = data.replace( b"", - b'', + b'', 1, ) elif file.suffix == ".html" and not (root.name == "search.gaiamobile.org" and path.startswith("/home/")): data = data.replace( b"", - b'', + b'', 1, ) if ( diff --git a/tools/test-camera.py b/tools/test-camera.py index cf3d2f7a..685e1308 100644 --- a/tools/test-camera.py +++ b/tools/test-camera.py @@ -76,12 +76,13 @@ with Client(2857) as c: "camera", 'return app.camera.mode==="picture" && app.camera.mozCamera.flashMode==="off";', ) - # Native touch injection targets the camera frame even on high-DPI desktops. - point = ev("camera", 'const r=document.querySelector(".js-capture").getBoundingClientRect();return {x:r.x+r.width/2,y:r.y+r.height/2};') + # Drive the desktop mouse through the normal touch simulator. Wait for + # layout after reload: a decoded video can precede a visible app frame. + point = wait("camera", 'const b=document.querySelector(".js-capture"),r=b.getBoundingClientRect(),x=r.x+r.width/2,y=r.y+r.height/2;return x>0 && y>0 && xdone(true),e=>done({error:String(e)}));""".replace('POINT',json.dumps(point)) +find(root).currentWindowGlobal.getActor('VulpesProbe').sendQuery('MouseClick',POINT).then(()=>done(true),e=>done({error:String(e)}));""".replace('POINT',json.dumps(point)) assert c.call('WebDriver:ExecuteAsyncScript',{'script':script,'args':[], 'newSandbox':True,'sandbox':'system','scriptTimeout':10000})['value'] is True photo = wait( diff --git a/tools/test-contacts.py b/tools/test-contacts.py new file mode 100644 index 00000000..3a8dd226 --- /dev/null +++ b/tools/test-contacts.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Exercise Gaia Contacts and its SMS/dialer activities in the desktop test profile. + +Creates one temporary contact; never sends a message or starts a call. +""" +import importlib.util +import json +import time +import sys +from pathlib import Path +from marionette_client import Client + +spec = importlib.util.spec_from_file_location('probe', Path(__file__).with_name('probe-desktop.py')) +probe = importlib.util.module_from_spec(spec) +spec.loader.exec_module(probe) +results = {} +contact_id = None +photo_name = None +name = 'Vulpes QA ' + str(time.time_ns()) + +with Client(2857) as client: + client.socket.settimeout(90) + probe.register(client) + + def ev(app, code): + result = probe.evaluate(client, 'communications' if app == 'dialer' else app, code, '/dialer/' if app == 'dialer' else '/contacts/' if app == 'communications' else '/') + if 'error' in result: + raise AssertionError(result) + return result.get('value') + + def wait(app, code): + for _ in range(100): + try: + value = ev(app, code) + except AssertionError as error: + if 'Actor' not in str(error) or 'destroyed' not in str(error): + raise + value = None + if value: + return value + time.sleep(.15) + raise TimeoutError(app + ': ' + code) + + def click(app, selector): + print("click", app, selector, file=sys.stderr, flush=True) + point = wait(app, 'for(const e of document.querySelectorAll(' + json.dumps(selector) + ')) {' + 'const r=e.getBoundingClientRect(),x=r.x+r.width/2,y=r.y+r.height/2;' + 'if(x>0&&y>0&&xdone(true),e=>done({error:String(e)}));'''.replace('PREFIX', json.dumps('http://' + app + '.localhost:8765/')).replace('POINT', json.dumps(point)) + clicked = client.call('WebDriver:ExecuteAsyncScript', {'script': script, 'args': [], + 'newSandbox': True, 'sandbox': 'system', 'scriptTimeout': 10000})['value'] + assert clicked is True, clicked + + def field(selector, value): + wait('communications', 'return !!document.querySelector(' + json.dumps(selector) + ');') + ev('communications', 'const e=document.querySelector(' + json.dumps(selector) + ');' + 'e.value=' + json.dumps(value) + ';e.dispatchEvent(new Event("input",{bubbles:true}));return true;') + + def contact(): + return ev('communications', '''const r=navigator.mozContacts.find({filterBy:['givenName'], +filterOp:'equals',filterValue:NAME});return await new Promise((ok,no)=>{ +r.onsuccess=()=>ok(r.result[0]||null);r.onerror=()=>no(r.error);});'''.replace('NAME', json.dumps(name))) + + try: + wait('system', 'return document.body?.getAttribute("ready-state")==="fullyLoaded";') + ev('system', "ScreenManager.turnScreenOn(true);if(Service.query('locked'))await Service.request('unlock',{forcibly:true});await VulpesCompat.call('apps.launch',{manifestURL:'http://communications.localhost:8765/manifest.webapp',entryPoint:'contacts'});return true;") + wait('communications', 'return document.readyState==="complete"&&!document.hidden;') + ev('system', "await VulpesCompat.call('apps.launch',{manifestURL:'http://gallery.localhost:8765/manifest.webapp'});return true;") + wait('gallery', 'return document.readyState==="complete"&&!document.hidden;') + fixture='vulpes-contacts-qa-'+str(time.time_ns())+'.jpg' + photo_name=ev('gallery', 'const c=document.createElement("canvas");c.width=c.height=400;' + 'const ctx=c.getContext("2d");ctx.fillStyle="#2372ae";ctx.fillRect(0,0,400,400);' + 'const blob=await new Promise(r=>c.toBlob(r,"image/jpeg"));' + 'return await VulpesCompat.call("media.request",{operation:"add",type:"pictures",name:'+json.dumps(fixture)+',blob});') + ev('system', "await VulpesCompat.call('apps.launch',{manifestURL:'http://communications.localhost:8765/manifest.webapp',entryPoint:'contacts'});return true;") + token = str(time.time_ns()) + ev('communications', 'setTimeout(()=>location.href=location.origin+"/contacts/index.html?qa='+token+'",0);return true;') + wait('communications', 'return location.search==="?qa='+token+'" && document.readyState==="complete";') + wait('communications', 'return typeof Loader!=="undefined" && !!window.Contacts && MainNavigation.currentView()==="view-contacts-list";') + click('communications', '#add-contact-button') + wait('communications', 'return window.MainNavigation?.currentView()==="view-contact-form" && !!document.querySelector("#number_0");') + field('#givenName', name) + field('#familyName', 'Élodie Test') + field('#number_0', '5550199') + field('#email_0', 'qa@example.invalid') + click('communications', '#save-button') + for _ in range(80): + row = contact() + if row: + contact_id = row['id'] + break + time.sleep(.15) + assert contact_id + results['create'] = row['tel'][0]['value'] == '5550199' + item = '[data-uuid=' + json.dumps(contact_id) + ']' + click('communications', '#search-start') + wait('communications', "return MainNavigation.currentView()==='search-view' && !!document.querySelector('#search-contact');") + field('#search-contact', 'elodie') + results['search_without_accent'] = bool(wait('communications', 'return !!document.querySelector(' + json.dumps('#search-list [data-uuid="' + contact_id + '"]') + ');')) + field('#search-contact', name + ' absent') + results['search_no_results'] = bool(wait('communications', "return !document.querySelector('#no-result').classList.contains('hide');")) + click('communications', '#cancel-search') + wait('communications', "return MainNavigation.currentView()==='view-contacts-list';") + click('communications', item) + click('communications', '#edit-contact-button') + wait('communications', 'return MainNavigation.currentView()==="view-contact-form";') + click('communications', '#photo-button') + wait('gallery', 'return !document.hidden && typeof picking!=="undefined" && picking;') + thumb='.thumbnailImage[data-filename='+json.dumps(photo_name)+']' + click('gallery',thumb) + wait('gallery','return !document.querySelector("#crop-done-button").disabled;') + click('gallery','#crop-done-button') + wait('communications', 'return !document.hidden && getComputedStyle(document.querySelector("#thumbnail-photo")).backgroundImage!=="none";') + field('#familyName', 'Élodie Modifiée') + click('communications', '#save-button') + results['edit_detail_refresh'] = bool(wait('communications', 'return document.querySelector("#contact-name-title")?.textContent.includes("Élodie Modifiée");')) + assert contact()['familyName'] == ['Élodie Modifiée'] + results['contact_photo']=bool(ev('communications', 'const r=navigator.mozContacts.find({filterBy:["id"],filterOp:"equals",filterValue:'+json.dumps(contact_id)+'});const c=await new Promise(ok=>{r.onsuccess=()=>ok(r.result[0]);});return c.photo?.[0] instanceof Blob && c.photo[0].size>0;')) + assert results['contact_photo'] + ev('communications', 'document.querySelector("#toggle-favorite").scrollIntoView({block:"center"});return true;') + click('communications', '#toggle-favorite') + results['favorite'] = bool(wait('communications', 'const r=navigator.mozContacts.find({filterBy:["id"],filterOp:"equals",filterValue:'+json.dumps(contact_id)+'});const c=await new Promise(ok=>{r.onsuccess=()=>ok(r.result[0]);});return c.category?.includes("favorite");')) + ev('communications', 'document.querySelector("#send-sms-button-0").scrollIntoView({block:"center"});return true;') + click('communications', '#send-sms-button-0') + results['sms_recipient'] = bool(wait('sms', 'return !document.hidden&&window.ConversationView?.recipients?.numbers.includes("5550199");')) + click('sms', '#messages-contact-pick-button') + wait('communications', 'return !document.hidden&&ActivityHandler.currentlyHandling;') + click('communications', item) + wait('communications', 'return !!document.querySelector("[data-l10n-id=pick_destination]");') + click('communications', '[data-l10n-id=pick_destination]') + wait('communications', 'return !ActivityHandler.currentlyHandling;') + results['picker_return'] = bool(wait('sms', 'return !document.hidden&&ConversationView.recipients.numbers.includes("5550199");')) + # This activity only prefills the original dialer; do not press Call. + ev('sms', 'window.__dialQA=new MozActivity({name:"dial",data:{type:"webtelephony/number",number:"5550199"}});return true;') + results['dialer_prefill'] = bool(wait('dialer', 'return !document.hidden&&document.querySelector("#phone-number-view")?.value==="5550199";')) + ev('system', "await VulpesCompat.call('apps.launch',{manifestURL:'http://communications.localhost:8765/manifest.webapp',entryPoint:'contacts'});return true;") + wait('communications', 'return !document.hidden;') + click('communications', item) + click('communications', '#edit-contact-button') + wait('communications', 'return MainNavigation.currentView()==="view-contact-form";') + ev('communications', 'document.querySelector("#delete-contact").scrollIntoView({block:"center"});return true;') + click('communications', '#delete-contact') + click('communications', '#confirmation-message button[data-l10n-id=cancel]') + results['delete_cancel'] = contact() is not None + wait('communications', 'return document.querySelector("#confirmation-message").classList.contains("hide");') + click('communications', '#delete-contact') + click('communications', '#confirmation-message button[data-l10n-id=delete]') + for _ in range(100): + if contact() is None: + results['delete_confirm'] = True + results['temporary_contact_removed'] = True + contact_id = None + break + time.sleep(.15) + assert results.get('delete_confirm') + results['passed'] = all(results.values()) + finally: + try: + if contact_id: + # Return to the address book before removing only our temporary entry. + ev('system', "await VulpesCompat.call('apps.launch',{manifestURL:'http://communications.localhost:8765/manifest.webapp',entryPoint:'contacts'});return true;") + ev('communications', 'const r=navigator.mozContacts.remove(' + json.dumps(contact_id) + ');await new Promise((ok,no)=>{r.onsuccess=ok;r.onerror=()=>no(r.error);});return true;') + results['temporary_contact_removed'] = contact() is None + except Exception as error: + results['cleanup_error'] = str(error) + results['temporary_contact_id'] = contact_id + finally: + if photo_name: + try: + ev('gallery','await VulpesCompat.call("media.request",{operation:"delete",type:"pictures",name:'+json.dumps(photo_name)+'});return true;') + results['temporary_photo_removed']=True + except Exception as error: + results['photo_cleanup_error']=str(error) + results['temporary_photo_name']=photo_name + print(json.dumps(results, indent=2, ensure_ascii=False)) + +if not results.get('passed') or results.get('cleanup_error') or results.get('photo_cleanup_error'): + raise SystemExit('Contacts qualification failed; see the report above.') diff --git a/tools/test-system-banner.py b/tools/test-system-banner.py new file mode 100644 index 00000000..ceb9a395 --- /dev/null +++ b/tools/test-system-banner.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Exercise Gaia's transient banner in the isolated desktop host.""" +import importlib.util +import json +from pathlib import Path +from marionette_client import Client +spec = importlib.util.spec_from_file_location('probe', Path(__file__).with_name('probe-desktop.py')) +probe = importlib.util.module_from_spec(spec) +spec.loader.exec_module(probe) +with Client(2857) as client: + probe.register(client) + result = probe.evaluate(client, 'system', ''' +await LazyLoader.load('js/system_banner.js'); +const banner=new SystemBanner(); +const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms)); +try { + await banner.show({id:'app-install-success',args:{appName:'Vulpes test'}}); + const shown=banner._banner.isConnected; + await sleep(4900); + const dismissed=!banner._banner.isConnected; + await banner.show('app-install-success'); + await sleep(1000); + await banner.show('app-install-success'); + await sleep(3300); + const replacementRetained=banner._banner.isConnected; + await sleep(1600); + const replacementDismissed=!banner._banner.isConnected; + await banner.show('app-install-success'); + banner._banner.click(); + await sleep(800); + const clickDismissed=!banner._banner.isConnected; + return {shown,dismissed,replacementRetained,replacementDismissed,clickDismissed}; +} finally {await banner.hide();banner._banner?.remove();} +''') + assert 'error' not in result, result + checks=result['value'] + assert all(checks.values()), checks + print(json.dumps(checks, indent=2)) diff --git a/tundra/boot/sparse-backup.c b/tundra/boot/sparse-backup.c new file mode 100644 index 00000000..bfb82c98 --- /dev/null +++ b/tundra/boot/sparse-backup.c @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: MPL-2.0 + * Read stdin only. Emit offsets and nonzero 64 KiB blocks for a host-side backup. + * No device is opened or written by this process. + */ +#define _start unused_stage0_start +#include "init.c" +#undef _start +static unsigned long buffer[8192]; +static void stop(long code) { + call(NR_exit, code, 0, 0, 0, 0); + for (;;) {} +} +static void write_all(const void *data, long length) { + const char *p = data; + while (length) { + long n = call(NR_write, 1, (long)p, length, 0, 0); + if (n == -4) continue; + if (n <= 0) stop(3); + p += n; + length -= n; + } +} +__attribute__((noreturn)) void _start(void) { + unsigned long offset = 0; + write_all("TUNRAW1\0", 8); + for (;;) { + long n = call(NR_read, 0, (long)buffer, sizeof(buffer), 0, 0); + if (n == -4) continue; + if (n < 0) stop(2); + if (!n) break; + unsigned long bits = 0; + long i = 0; + for (; i + 8 <= n; i += 8) bits |= buffer[i / 8]; + for (; i < n; ++i) bits |= ((unsigned char *)buffer)[i]; + if (bits) { + unsigned long header[2] = {offset, (unsigned long)n}; + write_all(header, sizeof(header)); + write_all(buffer, n); + } + offset += n; + } + unsigned long end[2] = {offset, 0}; + write_all(end, sizeof(end)); + call(NR_exit, 0, 0, 0, 0, 0); + for (;;) {} +} diff --git a/tundra/boot/standalone-mount.sh b/tundra/boot/standalone-mount.sh new file mode 100644 index 00000000..384d0438 --- /dev/null +++ b/tundra/boot/standalone-mount.sh @@ -0,0 +1,14 @@ +#!/bin/busybox sh +# SPDX-License-Identifier: MPL-2.0 +# Mount only the prepared Tundra volume. Never format or guess an old filesystem. +set -eu +# VOLUME_CONFIGURATION +mkdir -p /volume +blkid "$VOLUME_DEVICE" | grep -F "UUID=\"$VOLUME_UUID\"" >/dev/null +repaired=0 +e2fsck -p "$VOLUME_DEVICE" || repaired=$? +test "$repaired" -le 1 +mount -t ext4 -o "$VOLUME_OPTIONS" "$VOLUME_DEVICE" /volume +test "$(cat /volume/tundra-volume-id)" = "$VOLUME_UUID" +echo TUNDRA_STANDALONE_VOLUME_READY +exec /bin/sh /native-root.sh diff --git a/tundra/droidian/probe.sys.mjs b/tundra/droidian/probe.sys.mjs index 0949a2aa..2879a786 100644 --- a/tundra/droidian/probe.sys.mjs +++ b/tundra/droidian/probe.sys.mjs @@ -67,8 +67,10 @@ export async function run(win) { .then(async response=>dump(`TUNDRA_HTTP ${response.status} ${(await response.text()).length}\n`)) .catch(error=>dump('TUNDRA_HTTP_ERROR '+error+' offline='+Services.io.offline+'\n')); async function evaluate(app, code, message = 'Evaluate') { + const prefix=app==='contacts'?'http://communications.localhost:8765/contacts/': + app==='dialer'?'http://communications.localhost:8765/dialer/':`http://${app}.localhost:8765/`; function find(context) { - if(context.currentWindowGlobal?.documentURI.spec.startsWith(`http://${app}.localhost:8765/`)) return context; + if(context.currentWindowGlobal?.documentURI.spec.startsWith(prefix)) return context; for(const child of context.children) {const found=find(child); if(found) return found;} return null; } @@ -90,14 +92,15 @@ export async function run(win) { } throw new Error(`${app}: timeout: ${last}`); } - async function tap(app, selector) { - if(app==='keyboard') selector='.keyboard-type-container[data-active] '+selector; - await wait(app, `const e=document.querySelector(${JSON.stringify(selector)});if(!e)return false;const r=e.getBoundingClientRect();return r.width>0 && r.height>0 && e.contains(document.elementFromPoint(r.x+r.width/2,r.y+r.height/2));`); - await sleep(600); - const point = await evaluate(app, `const e=document.querySelector(${JSON.stringify(selector)});const r=e.getBoundingClientRect();return {x:r.x+r.width/2,y:r.y+r.height/2};`); - await evaluate(app,point,'Tap'); - await sleep(350); - } + async function tap(app, selector) { + if(app==='keyboard') selector='.keyboard-type-container[data-active] '+selector; + const point=await wait(app, `for(const e of document.querySelectorAll(${JSON.stringify(selector)})) { + const r=e.getBoundingClientRect(),x=r.x+r.width/2,y=r.y+r.height/2; + if(x>0 && y>0 && x0;"); - // Gaia remembers the selected lens; establish a known starting state. - await evaluate('camera', "if(app.settings.cameras.selected('key')!=='back')app.settings.cameras.next();return true;"); - await wait('camera', "return app.settings.cameras.selected('key')==='back' && !!app.camera.mozCamera && !app.camera.isBusy && document.querySelector('.viewfinder video')?.videoWidth>0;"); - cameraReport.gaia = await evaluate('camera', ` - const video=document.querySelector('video'); - const camera=app.camera.mozCamera; - window.__cameraBeforeSwitch=camera; - const capabilities=camera.capabilities; - let focus=null; - if(capabilities.maxFocusAreas>0) { - camera.setFocusAreas([{top:-150,left:-150,bottom:150,right:150}]); - await camera.autoFocus();focus=true; - } - if(capabilities.zoomRatios.length>1) { - camera.zoom=2; - await new Promise(r=>setTimeout(r,700)); - } - const testedZoom=camera.zoom; - camera.zoom=1; - const photo=await camera.takePicture(); - const bitmap=await createImageBitmap(photo); - const picture={width:bitmap.width,height:bitmap.height};bitmap.close(); - return {width:video.videoWidth,height:video.videoHeight,native:camera.vulpesNative===true, - picture,focus,zoom:testedZoom,focusAreas:capabilities.maxFocusAreas, - jpegBytes:photo.size,jpegType:photo.type,errors:window.__vulpesDiagnostics}; - `); - cameraReport.performance = await evaluate('camera', ` - const stream=app.camera.mozCamera, before=stream.vulpesTiming?.frames||0; - const start=performance.now();await new Promise(r=>setTimeout(r,4000)); - const elapsed=performance.now()-start; - const hud=document.querySelector('.hud'); - const button=document.querySelector('.js-camera'); - return {fps:(stream.vulpesTiming.frames-before)*1000/elapsed, - conversionMs:stream.vulpesTiming.conversionMs/stream.vulpesTiming.frames, - switchVisible:getComputedStyle(button).opacity!=='0' && hud.getAttribute('camera-enabled')==='true', - flashVisible:getComputedStyle(document.querySelector('.js-flash')).visibility==='visible'}; - `); - await evaluate('camera', "document.querySelector('.js-camera').click();return true;"); - await wait('camera', "return !!app.camera.mozCamera && app.camera.mozCamera!==window.__cameraBeforeSwitch && !app.camera.isBusy && document.querySelector('.viewfinder video')?.videoWidth>0;"); - cameraReport.switched = await evaluate('camera', ` - const photo=await app.camera.mozCamera.takePicture(); - window.__cameraBeforeHome=app.camera.mozCamera; - return {camera:app.settings.cameras.selected('key'),jpegBytes:photo.size, - previousStopped:window.__cameraBeforeSwitch.getTracks().every(t=>t.readyState==='ended')}; - `); - cameraReport.roundTrips = []; - for (let i=0; i<4; i++) { - await evaluate('camera', "window.__switchFrom=app.camera.mozCamera;return true;"); - await tap('camera', '.js-camera'); - await wait('camera', "return !!app.camera.mozCamera && app.camera.mozCamera!==window.__switchFrom && !app.camera.isBusy && document.querySelector('.viewfinder video')?.videoWidth>0;"); - cameraReport.roundTrips.push(await evaluate('camera', ` - const camera=app.camera.mozCamera, before=camera.vulpesTiming.frames; - await new Promise(r=>setTimeout(r,700)); - return {camera:app.settings.cameras.selected('key'), - frames:camera.vulpesTiming.frames-before, - previousStopped:window.__switchFrom.getTracks().every(t=>t.readyState==='ended')}; - `)); - } - await evaluate('camera', 'window.__cameraBeforeHome=app.camera.mozCamera;return true;'); - // Test Gaia's Home action, independent of a held pointer on the shell button. - cameraReport.beforeHome = await evaluate('system', "return {active:Service.query('getTopMostWindow')?.origin, top:Service.query('getTopMostUI')?.name, locked:Service.query('locked'), screen:ScreenManager.screenEnabled};"); - await evaluate('system', "dispatchEvent(new CustomEvent('home'));return true;"); - cameraReport.afterHome = await evaluate('system', "await new Promise(r=>setTimeout(r,1000));return {active:Service.query('getTopMostWindow')?.origin, top:Service.query('getTopMostUI')?.name, locked:Service.query('locked'), screen:ScreenManager.screenEnabled};"); - cameraReport.hidden = await wait('camera', 'return document.hidden;'); - cameraReport.stopped = await wait('camera', "return window.__cameraBeforeHome.getTracks().every(t=>t.readyState==='ended');"); - cameraReport.passed=cameraReport.gaia.native && cameraReport.gaia.picture.width*cameraReport.gaia.picture.height>10000000 && cameraReport.gaia.focus && cameraReport.gaia.zoom>1 && cameraReport.gaia.jpegBytes>1000 && cameraReport.gaia.jpegType==='image/jpeg' && cameraReport.switched.jpegBytes>1000 && cameraReport.switched.previousStopped && cameraReport.stopped && cameraReport.roundTrips.every(r=>r.frames>0 && r.previousStopped); - } catch(error) { cameraReport.error=String(error); } - finally { win.document.getElementById('home')?.click(); } - await IOUtils.writeJSON(`${root}/logs/camera.json`, cameraReport); - } - const smsMarker = `${root}/logs/test-sms-display`; - if (await IOUtils.exists(smsMarker)) { - await IOUtils.remove(smsMarker); - const smsReport = {bootId:report.bootId, passed:false, sent:false}; - try { - await evaluate('system', "ScreenManager.turnScreenOn(true);if(Service.query('locked'))await Service.request('unlock',{forcibly:true});await VulpesCompat.call('apps.launch',{manifestURL:'http://sms.localhost:8765/manifest.webapp'});return true;"); - await wait('sms', "return !!window.Navigation && !!window.MessageManager;"); - smsReport.threads = await evaluate('sms', ` - const threads=await new Promise((resolve,reject)=>{const rows=[];const c=navigator.mozMobileMessage.getThreads();c.onsuccess=()=>{if(c.done)resolve(rows);else{rows.push(c.result);c.continue();}};c.onerror=()=>reject(c.error);}); - window.__smsReviewThreads=threads.map(t=>t.id); - return {count:threads.length,validDates:threads.every(t=>Number.isFinite(+t.timestamp))}; - `); - smsReport.conversations=[]; - for(let i=0;iNumber.isFinite(+e.querySelector('time')?.dataset.time)), - nonempty:nodes.every(e=>!!e.querySelector('.message-content-body')?.textContent), - errorCount:window.__vulpesDiagnostics?.length||0}; - `)); - } - smsReport.passed=smsReport.threads.validDates && smsReport.conversations.length>0 && smsReport.conversations.every(c=>c.bubbles>0 && c.validDates && c.nonempty); - } catch(error) {smsReport.error=String(error);} - finally {win.document.getElementById('home')?.click();} - await IOUtils.writeJSON(`${root}/logs/sms-display.json`,smsReport); - } - const inputMarker = `${root}/logs/test-keyboard`; - if (await IOUtils.exists(inputMarker)) { - await IOUtils.remove(inputMarker); - const inputReport = {bootId:report.bootId,recordedAt:Date.now(),passed:false,sent:false}; - try { - await evaluate('system', 'ScreenManager.turnScreenOn(true);if(Service.query("locked"))await Service.request("unlock",{forcibly:true});return true;'); - await sleep(1500); - await evaluate('system', `await VulpesCompat.call('apps.launch',{manifestURL:'http://sms.localhost:8765/manifest.webapp'});return true;`); - await wait('sms', 'return document.querySelector("#threads-composer-link") && innerWidth>0;'); - await evaluate('sms', 'await Navigation.toPanel("thread-list");await Navigation.toPanel("composer");return true;'); - await wait('sms','return !!document.querySelector(".recipient[contenteditable]");'); - await tap('sms','.recipient[contenteditable]'); - await wait('keyboard', 'return navigator.mozInputMethod?.inputcontext?.inputType==="tel" && !!document.querySelector("button[aria-label=\\"2\\"]");'); - await tap('keyboard','button[aria-label="2"]'); - await tap('keyboard','button[aria-label="3"]'); - inputReport.recipientTyped = await evaluate('sms','return document.querySelector(".recipient[contenteditable]").textContent.endsWith("23");'); - const recipientBefore=await evaluate('sms','return document.querySelector(".recipient[contenteditable]").textContent;'); - await tap('sms','#messages-input'); - await wait('keyboard', 'return navigator.mozInputMethod?.inputcontext?.inputType==="text" && !!document.querySelector("button[aria-label=\\"a\\"]");'); - inputReport.keyboard = await evaluate('keyboard', `const e=document.querySelector('button[aria-label="a"]');const r=e.getBoundingClientRect(); - window.__keyTrial=[];for(const type of ['touchstart','touchend','pointerdown','click']) e.addEventListener(type,()=>__keyTrial.push(type),{once:true}); - return {width:innerWidth,height:innerHeight,key:r.toJSON(),font:getComputedStyle(document.documentElement).fontSize,portrait:app.viewManager.screenInPortraitMode(),engine:app.inputMethodManager.currentIMEngine?.constructor?.name};`); - await tap('keyboard','button[aria-label="a"]'); - await sleep(2000); - inputReport.context = await evaluate('keyboard', 'const c=navigator.mozInputMethod.inputcontext;return c&&{id:c.id,type:c.type,inputType:c.inputType,textLength:c.text.length};'); - inputReport.message = await evaluate('sms', 'const text=document.querySelector("#messages-input").textContent;return {length:text.length,lowercase:text.includes("a"),uppercase:text.includes("A")};'); - inputReport.messageTyped = inputReport.message.lowercase || inputReport.message.uppercase; - inputReport.events = await evaluate('keyboard','return window.__keyTrial;'); - inputReport.recipientUnchanged=await evaluate('sms',`return document.querySelector(".recipient[contenteditable]").textContent===${JSON.stringify(recipientBefore)};`); - inputReport.passed=inputReport.recipientTyped && inputReport.messageTyped && inputReport.recipientUnchanged && inputReport.keyboard.portrait && inputReport.keyboard.key.height>=44; - if(!inputReport.messageTyped) { - try { - await evaluate('keyboard','await navigator.mozInputMethod.inputcontext.sendKey(0,97);return true;'); - inputReport.directKey = await evaluate('sms','return document.querySelector("#messages-input").textContent.length>0;'); - } catch(error) {inputReport.directKeyError=String(error);} - } - inputReport.fonts=await evaluate('sms',null,'RenderedFonts'); - for (const face of inputReport.fonts) delete face.samples; - } catch(error) {inputReport.error=String(error);} - finally {win.document.getElementById('home').click();} - await IOUtils.writeJSON(`${root}/logs/keyboard.json`,inputReport); - } - const sendMarker=`${root}/logs/test-send-sms.json`; - if(await IOUtils.exists(sendMarker)) { - const request=await IOUtils.readJSON(sendMarker); - await IOUtils.remove(sendMarker); - const result={bootId:report.bootId,recordedAt:Date.now(),submitted:false,deliveryConfirmed:false}; - try { - if(typeof request.recipient!=='string' || !/^\+[0-9]{8,15}$/.test(request.recipient)) throw Error('INVALID_TEST_RECIPIENT'); - const {tundraRadio}=ChromeUtils.importESModule('resource://vulpes/host/Tundra.sys.mjs'); - const state=await tundraRadio('status'); - if(!['registered','roaming'].includes(state.registration?.[0]?.Status?.data)) throw Error('RADIO_NOT_REGISTERED'); - await evaluate('system', 'ScreenManager.turnScreenOn(true);if(Service.query("locked"))await Service.request("unlock",{forcibly:true});return true;'); - await sleep(1000); - await evaluate('system', `await VulpesCompat.call('apps.launch',{manifestURL:'http://sms.localhost:8765/manifest.webapp'});return true;`); - await wait('sms','return !!window.Navigation;'); - await evaluate('sms','await Navigation.toPanel("thread-list");await Navigation.toPanel("composer");return true;'); - await evaluate('sms','if(ConversationView.recipients.length || document.querySelector(".recipient[contenteditable]").textContent.trim() || Compose.getText())throw Error("TEST_DRAFT_PRESENT");return true;'); - await tap('sms','.recipient[contenteditable]'); - await wait('keyboard','return navigator.mozInputMethod?.inputcontext?.inputType==="tel";'); - await evaluate('keyboard',`await navigator.mozInputMethod.inputcontext.replaceSurroundingText(${JSON.stringify(request.recipient)},0,0);return true;`); - await tap('sms','#messages-input'); - await wait('keyboard','return navigator.mozInputMethod?.inputcontext?.inputType==="text";'); - const body='Vulpes OS - test SMS '+new Date().toISOString(); - await evaluate('keyboard',`await navigator.mozInputMethod.inputcontext.replaceSurroundingText(${JSON.stringify(body)},0,0);return true;`); - await wait('sms','return !document.querySelector("#messages-send-button").disabled;'); - await evaluate('sms',`if(!Navigation.isCurrentPanel('composer') || - JSON.stringify(ConversationView.recipients.numbers)!==JSON.stringify([${JSON.stringify(request.recipient)}]) || - Compose.getText()!==${JSON.stringify(body)})throw Error('TEST_MESSAGE_MISMATCH'); - document.querySelector('#messages-send-button').click();return true;`); - result.submitted=await wait('sms',`return await new Promise((resolve,reject)=>{ - const cursor=navigator.mozMobileMessage.getMessages(null,true); - cursor.onsuccess=()=>{const m=cursor.result;if(!m){resolve(false);return;} - if(m.body===${JSON.stringify(body)}){if(m.delivery==='error'){reject(Error('MODEM_TRANSMISSION_FAILED'));return;}resolve(m.delivery==='sent');return;}cursor.continue();}; - cursor.onerror=()=>reject(cursor.error); - });`); - } catch(error) {result.error=String(error);} - finally {win.document.getElementById('home').click();} - await IOUtils.writeJSON(`${root}/logs/send-sms.json`,result); - } - const notificationMarker = `${root}/logs/test-notifications`; - if (await IOUtils.exists(notificationMarker)) { - await IOUtils.remove(notificationMarker); - const result = {bootId:report.bootId,passed:false,silent:true}; - let id; - try { - await evaluate('system', `ScreenManager.turnScreenOn(true);await VulpesCompat.call('apps.launch',{manifestURL:'http://sms.localhost:8765/manifest.webapp'});return true;`); - await wait('sms', 'return typeof Notification.get === "function";'); - await evaluate('system', 'if(!Service.query("locked"))await Service.request("lock");return true;'); - result.lockState=await evaluate('system','return {service:Service.query("locked"),direct:lockScreen.locked,preview:NotificationScreen.lockscreenPreview};'); - await wait('system', 'return Service.query("locked") && NotificationScreen.lockscreenPreview;'); - id = await evaluate('sms', `return await new Promise((resolve,reject)=>{ - const n=new Notification('Vulpes — test local',{body:'Notification de qualification',tag:'vulpes-qualification',silent:true}); - n.onshow=()=>resolve(n.id);n.onerror=()=>reject(n.error); - });`); - const selector = `[data-notification-id="${id}"]`; - result.tray = await wait('system', `return !!NotificationScreen.container.querySelector(${JSON.stringify(selector)});`); - result.lockscreen = await wait('system', `return !!NotificationScreen.getLockScreenContainer()?.querySelector(${JSON.stringify(selector)});`); - await evaluate('sms', `const rows=await Notification.get({tag:'vulpes-qualification'});rows.forEach(n=>n.close());return true;`); - result.removed = await wait('system', `return !NotificationScreen.container.querySelector(${JSON.stringify(selector)}) && !NotificationScreen.getLockScreenContainer()?.querySelector(${JSON.stringify(selector)});`); - result.passed = result.tray && result.lockscreen && result.removed; - } catch(error) { result.error=String(error); } - finally { - if(id) await evaluate('sms', `for(const n of await Notification.get({tag:'vulpes-qualification'})) n.close();return true;`).catch(()=>{}); - win.document.getElementById('home').click(); - } - await IOUtils.writeJSON(`${root}/logs/notifications.json`,result); - } - } - await requestedUiChecks(); - let uiCheckRunning=false; - const uiCheckTimer=setInterval(async()=>{ - if(uiCheckRunning) return; - uiCheckRunning=true; - try { - const reloadMarker=`${root}/logs/test-reload-gaia`; - if(await IOUtils.exists(reloadMarker)) { - await IOUtils.remove(reloadMarker); - win.document.querySelector('browser').reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE); - await wait('system', "return document.body?.getAttribute('ready-state')==='fullyLoaded';"); - await sleep(1500); - } - await requestedUiChecks(); - } catch(error) {dump('TUNDRA_UI_CHECK_ERROR '+error+'\n');} - finally {uiCheckRunning=false;} - },3000); - win.addEventListener('unload',()=>clearInterval(uiCheckTimer),{once:true}); + async function requestedControls() { const controlsMarker = `${root}/logs/test-controls`; if (await IOUtils.exists(controlsMarker)) { await IOUtils.remove(controlsMarker); @@ -569,6 +338,392 @@ export async function run(win) { controls.brightness=brightnessCheck; await IOUtils.writeJSON(`${root}/logs/controls.json`, controls); } + } + async function requestedUiChecks() { + await requestedControls(); + const marketplaceMarker = `${root}/logs/test-marketplace-views`; + if (await IOUtils.exists(marketplaceMarker)) { + await IOUtils.remove(marketplaceMarker); + const result = {bootId:report.bootId,passed:false,checks:{},fixture:'http://127.0.0.1:8767/'}; + const {Views}=ChromeUtils.importESModule('resource://vulpes/host/Views.sys.mjs'); + const {Runtime}=ChromeUtils.importESModule('resource://vulpes/host/Runtime.sys.mjs'); + const until=async check=>{ + for(let i=0;i<80;i++){if(await check())return true;await sleep(250);} + throw Error('Marketplace view check timed out'); + }; + const visible=()=>[...Views.entries.values()].some(e=>e.browser.style.visibility==='visible'); + const open=async()=>{ + await evaluate('system', 'await VulpesCompat.call("apps.launch",{manifestURL:"http://marketplace.localhost:8765/manifest.webapp"});return true;'); + await wait('marketplace', 'return !!document.getElementById("browse");'); + await sleep(1000); + await evaluate('marketplace', 'document.getElementById("browse").click();document.getElementById("catalogue").src="http://127.0.0.1:8767/";return true;'); + await until(visible); + }; + try { + await evaluate('system', 'ScreenManager.turnScreenOn(true);if(Service.query("locked"))await Service.request("unlock",{forcibly:true});return true;'); + await sleep(1000); + // This fixture tests web-view ownership, not the lockscreen gesture. + // Service.unlock can leave the legacy overlay visible during clock teardown. + result.unlockFixture=await evaluate('system', 'let note=null;if(window.lockScreen){try{if(lockScreen.locked)lockScreen.unlock(true);}catch(e){note=String(e);}lockScreen.locked=false;lockScreen.overlay.classList.add("unlocked");}return {forced:true,cleanupError:note};'); + await open(); + result.checks.belowStatusBar=[...Views.entries.values()].filter(e=>e.browser.style.visibility==='visible').every(e=>parseFloat(e.browser.style.top)>=20); + await evaluate('marketplace', 'document.getElementById("catalogue").src="https://marketplace-offline.invalid/";return true;'); + result.checks.offlineMessage=await wait('marketplace', 'return !document.getElementById("message").hidden && /inaccessible|unavailable/.test(document.getElementById("status").textContent);'); + result.checks.errorHidden=await until(()=>!visible()); + await open(); + result.checks.retry=true; + win.document.getElementById('home').click(); + result.checks.home=await until(()=>!visible()); + await sleep(1000); + await open(); + Runtime.broadcast('holdhome',{},'system'); + await wait('system','return appWindowManager.taskManager.isActive();'); + result.checks.taskSwitcher=await until(()=>!visible()); + await sleep(1000); + await evaluate('system', 'const row=[...appWindowManager.taskManager.appToCardMap].find(([app])=>app.origin.includes("marketplace.localhost"));row[1].element.querySelector(".close-button").click();return true;'); + result.checks.closed=await until(()=>Views.entries.size===0); + result.passed=Object.values(result.checks).every(Boolean); + } catch(error) { + result.error=String(error); + result.views=[...Views.entries.values()].map(e=>({geometry:e.geometry,style:e.browser.style.cssText})); + } finally {win.document.getElementById('home').click();} + await IOUtils.writeJSON(`${root}/logs/marketplace-views.json`,result); + } + const ticketMarker = `${root}/logs/test-tickets-20261005`; + if (await IOUtils.exists(ticketMarker)) { + await IOUtils.remove(ticketMarker); + const result = {bootId:report.bootId,passed:false,checks:{}}; + try { + await evaluate('system', "ScreenManager.turnScreenOn(true);if(Service.query('locked'))await Service.request('unlock',{forcibly:true});return true;"); + await wait('system', "return !Service.query('locked');"); + await evaluate('system', "await VulpesCompat.call('apps.launch',{manifestURL:'http://settings.localhost:8765/manifest.webapp'});return true;"); + await wait('settings', "return document.readyState==='complete';"); + await evaluate('settings', "window.__ticketLanguage=(await VulpesCompat.call('settings.get',{key:'language.current'})).value;return true;"); + for (const lang of ['en-US','fr']) { + await evaluate('settings', `await VulpesCompat.call('settings.set',{values:{'language.current':${JSON.stringify(lang)}}});return true;`); + result.checks['language_'+lang] = await wait('homescreen', `return navigator.language===${JSON.stringify(lang)} && document.documentElement.lang===${JSON.stringify(lang)};`); + } + result.checks.developmentHidden = await evaluate('homescreen', "return [...document.querySelectorAll('gaia-app-icon')].every(e=>!/(uitest|test-|membuster|template|ds-test)/.test(e.app?.origin || ''));"); + result.checks.snap = await evaluate('homescreen', "return getComputedStyle(document.getElementById('panels')).scrollSnapType==='x mandatory';"); + await evaluate('settings', "window.__ticketPicker=new MozActivity({name:'pick',data:{type:'image/*'}});return true;"); + await wait('wallpaper', "return !!window.Wallpaper?.pickActivity && document.querySelectorAll('.wallpaper').length>1;"); + await evaluate('wallpaper', "document.querySelectorAll('.wallpaper')[1].click();return true;"); + result.checks.wallpaperPicker = await wait('settings', "return __ticketPicker.readyState==='done' && __ticketPicker.result?.blob instanceof Blob && __ticketPicker.result.blob.size>0;"); + await evaluate('settings', "window.__ticketPicker=new MozActivity({name:'pick',data:{type:'alerttone'}});return true;"); + await wait('ringtones', "return document.body.dataset.ready==='true' && document.querySelectorAll('gaia-radio').length>3;"); + await evaluate('ringtones', "document.querySelectorAll('gaia-radio')[2].click();return true;"); + await wait('ringtones', "return !document.getElementById('set').disabled;"); + await evaluate('ringtones', "document.getElementById('set').click();return true;"); + result.checks.tonePicker = await wait('settings', "return __ticketPicker.readyState==='done' && __ticketPicker.result?.blob instanceof Blob && __ticketPicker.result.blob.size>0;"); + result.passed=Object.values(result.checks).every(Boolean); + } catch(error) {result.error=String(error);} + finally { + await evaluate('settings', "if(window.__ticketLanguage)await VulpesCompat.call('settings.set',{values:{'language.current':__ticketLanguage}});return true;").catch(()=>{}); + win.document.getElementById('home').click(); + } + await IOUtils.writeJSON(`${root}/logs/tickets-20261005.json`,result); + } + const cameraMarker = `${root}/logs/test-camera`; + if (await IOUtils.exists(cameraMarker)) { + await IOUtils.remove(cameraMarker); + const cameraReport = {bootId:report.bootId, imagesRetained:false, passed:false}; + try { + await evaluate('system', "ScreenManager.turnScreenOn(true);if(Service.query('locked'))await Service.request('unlock',{forcibly:true});return true;"); + await wait('system', "return !Service.query('locked');"); + await evaluate('system', "await VulpesCompat.call('apps.launch',{manifestURL:'http://camera.localhost:8765/manifest.webapp'});return true;"); + await wait('camera', "return !!window.app?.camera?.mozCamera && document.querySelector('video')?.videoWidth>0;"); + // Gaia remembers the selected lens; establish a known starting state. + await evaluate('camera', "if(app.settings.cameras.selected('key')!=='back')app.settings.cameras.next();return true;"); + await wait('camera', "return app.settings.cameras.selected('key')==='back' && !!app.camera.mozCamera && !app.camera.isBusy && document.querySelector('.viewfinder video')?.videoWidth>0;"); + cameraReport.gaia = await evaluate('camera', ` + const video=document.querySelector('video'); + const camera=app.camera.mozCamera; + window.__cameraBeforeSwitch=camera; + const capabilities=camera.capabilities; + let focus=null, focusError=null; + if(capabilities.maxFocusAreas>0) { + try { + camera.setFocusAreas([{top:-150,left:-150,bottom:150,right:150}]); + await camera.autoFocus();focus=true; + } catch(error) { focus=false;focusError=String(error); } + } + if(capabilities.zoomRatios.length>1) { + camera.zoom=2; + await new Promise(r=>setTimeout(r,700)); + } + const testedZoom=camera.zoom; + camera.zoom=1; + const photo=await camera.takePicture(); + const bitmap=await createImageBitmap(photo); + const picture={width:bitmap.width,height:bitmap.height};bitmap.close(); + return {width:video.videoWidth,height:video.videoHeight,native:camera.vulpesNative===true, + picture,focus,focusError,zoom:testedZoom,focusAreas:capabilities.maxFocusAreas, + jpegBytes:photo.size,jpegType:photo.type,errors:window.__vulpesDiagnostics}; + `); + cameraReport.performance = await evaluate('camera', ` + const stream=app.camera.mozCamera, before=stream.vulpesTiming?.frames||0; + const start=performance.now();await new Promise(r=>setTimeout(r,4000)); + const elapsed=performance.now()-start; + const hud=document.querySelector('.hud'); + const button=document.querySelector('.js-camera'); + return {fps:(stream.vulpesTiming.frames-before)*1000/elapsed, + conversionMs:stream.vulpesTiming.conversionMs/stream.vulpesTiming.frames, + switchVisible:getComputedStyle(button).opacity!=='0' && hud.getAttribute('camera-enabled')==='true', + flashVisible:getComputedStyle(document.querySelector('.js-flash')).visibility==='visible'}; + `); + await evaluate('camera', "document.querySelector('.js-camera').click();return true;"); + await wait('camera', "return !!app.camera.mozCamera && app.camera.mozCamera!==window.__cameraBeforeSwitch && !app.camera.isBusy && document.querySelector('.viewfinder video')?.videoWidth>0;"); + cameraReport.switched = await evaluate('camera', ` + const photo=await app.camera.mozCamera.takePicture(); + window.__cameraBeforeHome=app.camera.mozCamera; + return {camera:app.settings.cameras.selected('key'),jpegBytes:photo.size, + previousStopped:window.__cameraBeforeSwitch.getTracks().every(t=>t.readyState==='ended')}; + `); + cameraReport.roundTrips = []; + for (let i=0; i<4; i++) { + await evaluate('camera', "window.__switchFrom=app.camera.mozCamera;return true;"); + await tap('camera', '.js-camera'); + await wait('camera', "return !!app.camera.mozCamera && app.camera.mozCamera!==window.__switchFrom && !app.camera.isBusy && document.querySelector('.viewfinder video')?.videoWidth>0;"); + cameraReport.roundTrips.push(await evaluate('camera', ` + const camera=app.camera.mozCamera, before=camera.vulpesTiming.frames; + await new Promise(r=>setTimeout(r,700)); + return {camera:app.settings.cameras.selected('key'), + frames:camera.vulpesTiming.frames-before, + previousStopped:window.__switchFrom.getTracks().every(t=>t.readyState==='ended')}; + `)); + } + await evaluate('camera', 'window.__cameraBeforeHome=app.camera.mozCamera;return true;'); + // Test Gaia's Home action, independent of a held pointer on the shell button. + await evaluate('camera', "window.__cameraFilesBefore=new Set((await VulpesCompat.call('media.request',{type:'pictures',operation:'enumerate'})).map(f=>f.name));return true;"); + await tap('camera','.js-capture'); + await wait('camera', "const files=await VulpesCompat.call('media.request',{type:'pictures',operation:'enumerate'});window.__cameraSavedPhoto=files.find(f=>!__cameraFilesBefore.has(f.name));return !!__cameraSavedPhoto;"); + cameraReport.savedPhoto=await evaluate('camera', 'return {name:__cameraSavedPhoto.name,size:__cameraSavedPhoto.size,type:__cameraSavedPhoto.type};'); + await evaluate('system', "await VulpesCompat.call('apps.launch',{manifestURL:'http://gallery.localhost:8765/manifest.webapp'});return true;"); + await wait('gallery', `return typeof files!=='undefined' && files.some(f=>f.name===${JSON.stringify(cameraReport.savedPhoto.name)} && f.metadata.width>0 && f.metadata.thumbnail);`); + await evaluate('gallery', `document.querySelector('[data-filename="'+${JSON.stringify(cameraReport.savedPhoto.name)}+'"]')?.click();return true;`); + cameraReport.galleryOpened=await wait('gallery', "return document.body.classList.contains('fullscreenView');"); + await evaluate('system', "await VulpesCompat.call('apps.launch',{manifestURL:'http://camera.localhost:8765/manifest.webapp'});return true;"); + await wait('camera', "return !!app.camera.mozCamera && !app.camera.isBusy && !document.hidden;"); + await evaluate('camera', `await navigator.getDeviceStorage('pictures').delete(${JSON.stringify(cameraReport.savedPhoto.name)});window.__cameraBeforeHome=app.camera.mozCamera;return true;`); + cameraReport.captureStored=cameraReport.savedPhoto.size>1000; + delete cameraReport.savedPhoto.name; + cameraReport.beforeHome = await evaluate('system', "return {active:Service.query('getTopMostWindow')?.origin, top:Service.query('getTopMostUI')?.name, locked:Service.query('locked'), screen:ScreenManager.screenEnabled};"); + await evaluate('system', "dispatchEvent(new CustomEvent('home'));return true;"); + cameraReport.afterHome = await evaluate('system', "await new Promise(r=>setTimeout(r,1000));return {active:Service.query('getTopMostWindow')?.origin, top:Service.query('getTopMostUI')?.name, locked:Service.query('locked'), screen:ScreenManager.screenEnabled};"); + cameraReport.hidden = await wait('camera', 'return document.hidden;'); + cameraReport.stopped = await wait('camera', "return window.__cameraBeforeHome.getTracks().every(t=>t.readyState==='ended');"); + cameraReport.passed=cameraReport.captureStored && cameraReport.galleryOpened && cameraReport.gaia.native && cameraReport.gaia.picture.width*cameraReport.gaia.picture.height>10000000 && cameraReport.gaia.focus && cameraReport.gaia.zoom>1 && cameraReport.gaia.jpegBytes>1000 && cameraReport.gaia.jpegType==='image/jpeg' && cameraReport.switched.jpegBytes>1000 && cameraReport.switched.previousStopped && cameraReport.stopped && cameraReport.roundTrips.every(r=>r.frames>0 && r.previousStopped); + } catch(error) { cameraReport.error=String(error); } + finally { win.document.getElementById('home')?.click(); } + await IOUtils.writeJSON(`${root}/logs/camera.json`, cameraReport); + } + const contactsMarker=`${root}/logs/test-contacts`; + if(await IOUtils.exists(contactsMarker)) { + await IOUtils.remove(contactsMarker); + const result={bootId:report.bootId,passed:false}, name='Vulpes QA '+Date.now(); + let id; + const lookup=`const r=navigator.mozContacts.find({filterBy:['givenName'],filterOp:'equals',filterValue:${JSON.stringify(name)}}); + return await new Promise((ok,no)=>{r.onsuccess=()=>ok(r.result[0]||null);r.onerror=()=>no(r.error);});`; + const field=async(selector,value)=>evaluate('contacts',`const e=document.querySelector(${JSON.stringify(selector)}); + e.value=${JSON.stringify(value)};e.dispatchEvent(new Event('input',{bubbles:true}));return true;`); + try { + await evaluate('system', "ScreenManager.turnScreenOn(true);if(Service.query('locked'))await Service.request('unlock',{forcibly:true});await VulpesCompat.call('apps.launch',{manifestURL:'http://communications.localhost:8765/manifest.webapp',entryPoint:'contacts'});return true;"); + await wait('contacts', "return typeof Loader!=='undefined' && window.MainNavigation?.currentView()==='view-contacts-list';"); + await tap('contacts','#add-contact-button'); + await wait('contacts', "return MainNavigation.currentView()==='view-contact-form' && !!document.querySelector('#number_0');"); + await field('#givenName',name);await field('#familyName','Élodie Test'); + await field('#number_0','5550199');await field('#email_0','qa@example.invalid'); + await tap('contacts','#save-button'); + const saved=await wait('contacts',lookup);id=saved.id; + result.created=saved.tel[0].value==='5550199'; + const selector='[data-uuid="'+id+'"]'; + await tap('contacts',selector);await tap('contacts','#edit-contact-button'); + await wait('contacts', "return MainNavigation.currentView()==='view-contact-form';"); + await field('#familyName','Élodie Modifiée');await tap('contacts','#save-button'); + result.edited=await wait('contacts', "return document.querySelector('#contact-name-title')?.textContent.includes('Élodie Modifiée');"); + await evaluate('contacts', "document.querySelector('#send-sms-button-0').scrollIntoView({block:'center'});return true;"); + await tap('contacts','#send-sms-button-0'); + result.sms=await wait('sms', "return !document.hidden && window.ConversationView?.recipients?.numbers.includes('5550199');"); + await tap('sms','#messages-contact-pick-button'); + await wait('contacts', 'return !document.hidden && ActivityHandler.currentlyHandling;'); + await tap('contacts',selector);await tap('contacts','[data-l10n-id=pick_destination]'); + await wait('contacts','return !ActivityHandler.currentlyHandling;'); + result.picker=await wait('sms', "return !document.hidden && ConversationView.recipients.numbers.includes('5550199');"); + await evaluate('sms', "new MozActivity({name:'dial',data:{type:'webtelephony/number',number:'5550199'}});return true;"); + result.dialer=await wait('dialer', "return !document.hidden && document.querySelector('#phone-number-view')?.value==='5550199';"); + result.passed=result.created&&result.edited&&result.sms&&result.picker&&result.dialer; + } catch(error) {result.error=String(error);} + finally { + try { + if(!id) id=(await evaluate('contacts',lookup))?.id; + if(id) { + await evaluate('contacts', `const r=navigator.mozContacts.remove(${JSON.stringify(id)});await new Promise((ok,no)=>{r.onsuccess=ok;r.onerror=()=>no(r.error);});return true;`); + result.temporaryContactRemoved=!(await evaluate('contacts',lookup)); + } + } catch(error) {result.cleanupError=String(error);} + win.document.getElementById('home')?.click(); + } + await IOUtils.writeJSON(`${root}/logs/contacts.json`,result); + } + const smsMarker = `${root}/logs/test-sms-display`; + if (await IOUtils.exists(smsMarker)) { + await IOUtils.remove(smsMarker); + const smsReport = {bootId:report.bootId, passed:false, sent:false}; + try { + await evaluate('system', "ScreenManager.turnScreenOn(true);if(Service.query('locked'))await Service.request('unlock',{forcibly:true});await VulpesCompat.call('apps.launch',{manifestURL:'http://sms.localhost:8765/manifest.webapp'});return true;"); + await wait('sms', "return !!window.Navigation && !!window.MessageManager;"); + smsReport.threads = await evaluate('sms', ` + const threads=await new Promise((resolve,reject)=>{const rows=[];const c=navigator.mozMobileMessage.getThreads();c.onsuccess=()=>{if(c.done)resolve(rows);else{rows.push(c.result);c.continue();}};c.onerror=()=>reject(c.error);}); + window.__smsReviewThreads=threads.map(t=>t.id); + return {count:threads.length,validDates:threads.every(t=>Number.isFinite(+t.timestamp))}; + `); + smsReport.conversations=[]; + for(let i=0;iNumber.isFinite(+e.querySelector('time')?.dataset.time)), + nonempty:nodes.every(e=>!!e.querySelector('.message-content-body')?.textContent), + errorCount:window.__vulpesDiagnostics?.length||0}; + `)); + } + smsReport.passed=smsReport.threads.validDates && smsReport.conversations.length>0 && smsReport.conversations.every(c=>c.bubbles>0 && c.validDates && c.nonempty); + } catch(error) {smsReport.error=String(error);} + finally {win.document.getElementById('home')?.click();} + await IOUtils.writeJSON(`${root}/logs/sms-display.json`,smsReport); + } + const inputMarker = `${root}/logs/test-keyboard`; + if (await IOUtils.exists(inputMarker)) { + await IOUtils.remove(inputMarker); + const inputReport = {bootId:report.bootId,recordedAt:Date.now(),passed:false,sent:false}; + try { + await evaluate('system', 'ScreenManager.turnScreenOn(true);if(Service.query("locked"))await Service.request("unlock",{forcibly:true});return true;'); + await sleep(1500); + await evaluate('system', `await VulpesCompat.call('apps.launch',{manifestURL:'http://sms.localhost:8765/manifest.webapp'});return true;`); + await wait('sms', 'return document.querySelector("#threads-composer-link") && innerWidth>0;'); + await evaluate('sms', 'await Navigation.toPanel("thread-list");await Navigation.toPanel("composer");return true;'); + await wait('sms','return !!document.querySelector(".recipient[contenteditable]");'); + await tap('sms','.recipient[contenteditable]'); + await wait('keyboard', 'return navigator.mozInputMethod?.inputcontext?.inputType==="tel" && !!document.querySelector("button[aria-label=\\"2\\"]");'); + await tap('keyboard','button[aria-label="2"]'); + await tap('keyboard','button[aria-label="3"]'); + inputReport.recipientTyped = await evaluate('sms','return document.querySelector(".recipient[contenteditable]").textContent.endsWith("23");'); + const recipientBefore=await evaluate('sms','return document.querySelector(".recipient[contenteditable]").textContent;'); + await tap('sms','#messages-input'); + await wait('keyboard', 'return navigator.mozInputMethod?.inputcontext?.inputType==="text" && !!document.querySelector("button[aria-label=\\"a\\"]");'); + inputReport.keyboard = await evaluate('keyboard', `const e=document.querySelector('button[aria-label="a"]');const r=e.getBoundingClientRect(); + window.__keyTrial=[];for(const type of ['touchstart','touchend','pointerdown','click']) e.addEventListener(type,()=>__keyTrial.push(type),{once:true}); + return {width:innerWidth,height:innerHeight,key:r.toJSON(),font:getComputedStyle(document.documentElement).fontSize,portrait:app.viewManager.screenInPortraitMode(),engine:app.inputMethodManager.currentIMEngine?.constructor?.name};`); + await tap('keyboard','button[aria-label="a"]'); + await sleep(2000); + inputReport.context = await evaluate('keyboard', 'const c=navigator.mozInputMethod.inputcontext;return c&&{id:c.id,type:c.type,inputType:c.inputType,textLength:c.text.length};'); + inputReport.message = await evaluate('sms', 'const text=document.querySelector("#messages-input").textContent;return {length:text.length,lowercase:text.includes("a"),uppercase:text.includes("A")};'); + inputReport.messageTyped = inputReport.message.lowercase || inputReport.message.uppercase; + inputReport.events = await evaluate('keyboard','return window.__keyTrial;'); + inputReport.recipientUnchanged=await evaluate('sms',`return document.querySelector(".recipient[contenteditable]").textContent===${JSON.stringify(recipientBefore)};`); + inputReport.passed=inputReport.recipientTyped && inputReport.messageTyped && inputReport.recipientUnchanged && inputReport.keyboard.portrait && inputReport.keyboard.key.height>=44; + if(!inputReport.messageTyped) { + try { + await evaluate('keyboard','await navigator.mozInputMethod.inputcontext.sendKey(0,97);return true;'); + inputReport.directKey = await evaluate('sms','return document.querySelector("#messages-input").textContent.length>0;'); + } catch(error) {inputReport.directKeyError=String(error);} + } + inputReport.fonts=await evaluate('sms',null,'RenderedFonts'); + for (const face of inputReport.fonts) delete face.samples; + } catch(error) {inputReport.error=String(error);} + finally {win.document.getElementById('home').click();} + await IOUtils.writeJSON(`${root}/logs/keyboard.json`,inputReport); + } + const sendMarker=`${root}/logs/test-send-sms.json`; + if(await IOUtils.exists(sendMarker)) { + const request=await IOUtils.readJSON(sendMarker); + await IOUtils.remove(sendMarker); + const result={bootId:report.bootId,recordedAt:Date.now(),submitted:false,deliveryConfirmed:false}; + try { + if(typeof request.recipient!=='string' || !/^\+[0-9]{8,15}$/.test(request.recipient)) throw Error('INVALID_TEST_RECIPIENT'); + const {tundraRadio}=ChromeUtils.importESModule('resource://vulpes/host/Tundra.sys.mjs'); + const state=await tundraRadio('status'); + if(!['registered','roaming'].includes(state.registration?.[0]?.Status?.data)) throw Error('RADIO_NOT_REGISTERED'); + await evaluate('system', 'ScreenManager.turnScreenOn(true);if(Service.query("locked"))await Service.request("unlock",{forcibly:true});return true;'); + await sleep(1000); + await evaluate('system', `await VulpesCompat.call('apps.launch',{manifestURL:'http://sms.localhost:8765/manifest.webapp'});return true;`); + await wait('sms','return !!window.Navigation;'); + await evaluate('sms','await Navigation.toPanel("thread-list");await Navigation.toPanel("composer");return true;'); + await evaluate('sms','if(ConversationView.recipients.length || document.querySelector(".recipient[contenteditable]").textContent.trim() || Compose.getText())throw Error("TEST_DRAFT_PRESENT");return true;'); + await tap('sms','.recipient[contenteditable]'); + await wait('keyboard','return navigator.mozInputMethod?.inputcontext?.inputType==="tel";'); + await evaluate('keyboard',`await navigator.mozInputMethod.inputcontext.replaceSurroundingText(${JSON.stringify(request.recipient)},0,0);return true;`); + await tap('sms','#messages-input'); + await wait('keyboard','return navigator.mozInputMethod?.inputcontext?.inputType==="text";'); + const body='Vulpes OS - test SMS '+new Date().toISOString(); + await evaluate('keyboard',`await navigator.mozInputMethod.inputcontext.replaceSurroundingText(${JSON.stringify(body)},0,0);return true;`); + await wait('sms','return !document.querySelector("#messages-send-button").disabled;'); + result.preSend=await evaluate('sms',`return {composer:Navigation.isCurrentPanel('composer'), + recipientMatch:JSON.stringify(ConversationView.recipients.numbers.map(n=>navigator.mozPhoneNumberService.normalize(n)))===JSON.stringify([${JSON.stringify(request.recipient)}]), + recipientCount:ConversationView.recipients.numbers.length, + bodyMatch:Compose.getText()===${JSON.stringify(body)},bodyLength:Compose.getText().length, + expectedLength:${body.length}};`); + if(!result.preSend.composer || !result.preSend.recipientMatch || !result.preSend.bodyMatch) + throw Error('TEST_MESSAGE_MISMATCH'); + await evaluate('sms',"document.querySelector('#messages-send-button').click();return true;"); + result.submitted=await wait('sms',`return await new Promise((resolve,reject)=>{ + const cursor=navigator.mozMobileMessage.getMessages(null,true); + cursor.onsuccess=()=>{const m=cursor.result;if(!m){resolve(false);return;} + if(m.body===${JSON.stringify(body)}){if(m.delivery==='error'){reject(Error('MODEM_TRANSMISSION_FAILED'));return;}resolve(m.delivery==='sent');return;}cursor.continue();}; + cursor.onerror=()=>reject(cursor.error); + });`); + } catch(error) {result.error=String(error);} + finally {win.document.getElementById('home').click();} + await IOUtils.writeJSON(`${root}/logs/send-sms.json`,result); + } + const notificationMarker = `${root}/logs/test-notifications`; + if (await IOUtils.exists(notificationMarker)) { + await IOUtils.remove(notificationMarker); + const result = {bootId:report.bootId,passed:false,silent:true}; + let id; + try { + await evaluate('system', `ScreenManager.turnScreenOn(true);await VulpesCompat.call('apps.launch',{manifestURL:'http://sms.localhost:8765/manifest.webapp'});return true;`); + await wait('sms', 'return typeof Notification.get === "function";'); + await evaluate('system', 'if(!Service.query("locked"))await Service.request("lock");return true;'); + result.lockState=await evaluate('system','return {service:Service.query("locked"),direct:lockScreen.locked,preview:NotificationScreen.lockscreenPreview};'); + await wait('system', 'return Service.query("locked") && NotificationScreen.lockscreenPreview;'); + id = await evaluate('sms', `return await new Promise((resolve,reject)=>{ + const n=new Notification('Vulpes — test local',{body:'Notification de qualification',tag:'vulpes-qualification',silent:true}); + n.onshow=()=>resolve(n.id);n.onerror=()=>reject(n.error); + });`); + const selector = `[data-notification-id="${id}"]`; + result.tray = await wait('system', `return !!NotificationScreen.container.querySelector(${JSON.stringify(selector)});`); + result.lockscreen = await wait('system', `return !!NotificationScreen.getLockScreenContainer()?.querySelector(${JSON.stringify(selector)});`); + await evaluate('sms', `const rows=await Notification.get({tag:'vulpes-qualification'});rows.forEach(n=>n.close());return true;`); + result.removed = await wait('system', `return !NotificationScreen.container.querySelector(${JSON.stringify(selector)}) && !NotificationScreen.getLockScreenContainer()?.querySelector(${JSON.stringify(selector)});`); + result.passed = result.tray && result.lockscreen && result.removed; + } catch(error) { result.error=String(error); } + finally { + if(id) await evaluate('sms', `for(const n of await Notification.get({tag:'vulpes-qualification'})) n.close();return true;`).catch(()=>{}); + win.document.getElementById('home').click(); + } + await IOUtils.writeJSON(`${root}/logs/notifications.json`,result); + } + } + await requestedUiChecks(); + let uiCheckRunning=false; + const uiCheckTimer=setInterval(async()=>{ + if(uiCheckRunning) return; + uiCheckRunning=true; + try { + const reloadMarker=`${root}/logs/test-reload-gaia`; + if(await IOUtils.exists(reloadMarker)) { + await IOUtils.remove(reloadMarker); + win.document.querySelector('browser').reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE); + await wait('system', "return document.body?.getAttribute('ready-state')==='fullyLoaded';"); + await sleep(1500); + } + await requestedUiChecks(); + } catch(error) {dump('TUNDRA_UI_CHECK_ERROR '+error+'\n');} + finally {uiCheckRunning=false;} + },3000); + win.addEventListener('unload',()=>clearInterval(uiCheckTimer),{once:true}); report.layout=await evaluate('homescreen',"return {width:innerWidth,height:innerHeight,dpr:devicePixelRatio,rootFont:getComputedStyle(document.documentElement).fontSize,bodyFont:getComputedStyle(document.body).fontFamily,zoom:visualViewport.scale,rootWidth:document.documentElement.scrollWidth};"); report.textFonts = await evaluate('homescreen', ` await document.fonts.ready; diff --git a/tundra/manifests/sargo-hardware.json b/tundra/manifests/sargo-hardware.json new file mode 100644 index 00000000..3e8748bd --- /dev/null +++ b/tundra/manifests/sargo-hardware.json @@ -0,0 +1,22 @@ +{ + "schema": 1, + "device": "sargo", + "scope": "Pinned hardware image inputs for local standalone builds; redistribution not reviewed", + "images": { + "vendor.img": { + "source": "vendor_a.img", + "sha256": "d584429873c88db741b81144b55977bbd8126b3e877445901bee6a0bda7fe4d0", + "bytes": 805306368 + }, + "modem.img": { + "source": "modem_a.img", + "sha256": "fd0624b37ac1d80c064a9763c7e2d2d6f94c3fd5e4617965b735ad411a473d0a", + "bytes": 115343360 + } + }, + "dtbo": { + "sha256": "0e43a57d6423e8d24cd944fcad0939ed81160e847a6d4203e2f7c16a86d69c03", + "bytes": 8417280 + }, + "vendorFingerprint": "google/sargo/sargo:9/PD2A.190115.029/5320088:user/release-keys" +} diff --git a/tundra/runtime/hal_policy.py b/tundra/runtime/hal_policy.py index 287a3c9e..8530ff0c 100644 --- a/tundra/runtime/hal_policy.py +++ b/tundra/runtime/hal_policy.py @@ -20,7 +20,7 @@ NETWORK_SERVICES = frozenset(('irsc_util', 'vendor.per_mgr', 'vendor.rmt_storage 'vendor.tftp_server', 'cnss-daemon', 'pd_mapper')) -HARDWARE_SERVICES = frozenset(('vendor.qcrild', 'vendor.google.radioext@1.0', +HARDWARE_SERVICES = frozenset(('vendor.bluetooth-1-0', 'vendor.qcrild', 'vendor.google.radioext@1.0', 'vendor.audio-hal-2-0', 'vendor.camera-provider-2-4', 'camera_service', 'minimedia', 'minisf', 'miniaf')) diff --git a/tundra/runtime/mark-boot-success.py b/tundra/runtime/mark-boot-success.py new file mode 100644 index 00000000..b1c2fa95 --- /dev/null +++ b/tundra/runtime/mark-boot-success.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MPL-2.0 +"""Acknowledge slot A only after this standalone installation has started Gaia.""" +import json +from pathlib import Path +import subprocess +import time + + +def eligible(boot_id, gaia, mount, cmdline, services_ready): + return (cmdline.get('androidboot.slot_suffix') == '_a' + and mount.get('source') == '/dev/mmcblk0p72' + and mount.get('fstype') == 'ext4' + and gaia.get('bootId') == boot_id and gaia.get('passed') is True + and bool(gaia.get('checks')) and all(gaia['checks'].values()) + and services_ready) + + +def main(): + cmdline=dict(x.split('=',1) for x in Path('/proc/cmdline').read_text().split() if '=' in x) + boot_id=Path('/proc/sys/kernel/random/boot_id').read_text().strip() + mounts=json.loads(subprocess.check_output(['findmnt','--json','--mountpoint','/reference/tundra-volume','--output','SOURCE,FSTYPE'],text=True))['filesystems'] + if len(mounts)!=1 or mounts[0]['source']!='/dev/mmcblk0p72': + return # Container/RAM experiments must not mark a flashed slot successful. + for _ in range(120): + try: + gaia=json.loads(Path('/opt/vulpes/logs/test.json').read_text()) + ready=subprocess.run(['systemctl','is-active','--quiet','tundra-hal','tundra-display','tundra-radio']).returncode==0 + if eligible(boot_id,gaia,mounts[0],cmdline,ready):break + except (OSError,ValueError):pass + time.sleep(5) + else:raise RuntimeError('Gaia did not pass startup; slot success left unchanged') + subprocess.run(['/usr/libexec/tundra-qbootctl','-m','a'],check=True) + Path('/var/lib/tundra').mkdir(parents=True,exist_ok=True) + Path('/var/lib/tundra/boot-success.json').write_text(json.dumps({'bootId':boot_id,'gaiaPassed':True,'slot':'a'})+'\n') + + +if __name__=='__main__':main() diff --git a/tundra/runtime/prepare-bluetooth.py b/tundra/runtime/prepare-bluetooth.py new file mode 100644 index 00000000..f69656e5 --- /dev/null +++ b/tundra/runtime/prepare-bluetooth.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: MPL-2.0 +"""Connect the Sargo Bluetooth HAL to BlueZ; pairing stays user initiated.""" + +def prepare(root, write): + write('etc/dbus-1/system.d/tundra-bluetooth.conf', ''' + +''') + write('etc/bluetooth/main.conf', '[General]\nName=Vulpes OS\nDiscoverableTimeout=120\n[Policy]\nAutoEnable=false\n') + write('etc/systemd/system/tundra-bluebinder.service', '''[Unit] +Description=Sargo Bluetooth HAL bridge +Requires=tundra-hal.service +After=tundra-hal.service +Before=bluetooth.service +[Service] +Type=notify +ExecStartPre=/usr/bin/binder-wait android.hardware.bluetooth@1.0::IBluetoothHci/default +ExecStart=/usr/sbin/bluebinder +Restart=on-failure +RestartSec=5 +TimeoutStartSec=60 +NoNewPrivileges=yes +ProtectHome=yes +ProtectSystem=strict +PrivateTmp=yes +''') + write('etc/systemd/system/bluetooth.service.d/tundra.conf', '''[Unit] +Requires=tundra-bluebinder.service dbus.service +After=tundra-bluebinder.service dbus.service +''') + wants = root/'etc/systemd/system/tundra.target.wants' + wants.mkdir(parents=True, exist_ok=True) + (wants/'bluetooth.service').symlink_to('/usr/lib/systemd/system/bluetooth.service') diff --git a/tundra/runtime/prepare-hardware.py b/tundra/runtime/prepare-hardware.py index 3b588f80..e5d790c3 100644 --- a/tundra/runtime/prepare-hardware.py +++ b/tundra/runtime/prepare-hardware.py @@ -3,10 +3,13 @@ """Configure isolated oFono/PulseAudio diagnostics; no call or SMS on startup.""" from pathlib import Path import shutil +import runpy def prepare(root, write): write('etc/tundra/hardware-enabled', 'sargo\n') + runpy.run_path(str(Path(__file__).with_name('runtime-packages.py')))['stage'](root) + runpy.run_path(str(Path(__file__).with_name('prepare-bluetooth.py')))['prepare'](root, write) shutil.copy2(Path(__file__).with_name('modem-recovery.py'), root/'etc/tundra/modem-recovery.py') write('etc/systemd/system/tundra-hal.service.d/modem-recovery.conf', '''[Service] diff --git a/tundra/runtime/runtime-packages.py b/tundra/runtime/runtime-packages.py new file mode 100644 index 00000000..904db1c0 --- /dev/null +++ b/tundra/runtime/runtime-packages.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MPL-2.0 +"""Stage pinned package payloads missing from the original ARM64 reference.""" +import hashlib +import io +import json +from pathlib import Path +import shutil +import subprocess +import tarfile +import tempfile +import urllib.request + +PACKAGES = ( + ('libdw1t64_0.196-1_arm64.deb', 'e/elfutils', 'a82634454de2b28d73374f693e4ab6c1c2634fa53b0139312c5f439c294dad2b'), + ('libelf1t64_0.196-1_arm64.deb', 'e/elfutils', 'ead8a1244824d982ce51fb82be4904312d5dc44ddc157e589def58c257738476'), + ('bluez_5.87-4_arm64.deb', 'b/bluez', 'fe733034dbdfa6146fc94112ad8ca0fce71d8e01d22a59ddb49ef2df3777d6b5'), + ('ca-certificates_20260816_all.deb', 'c/ca-certificates', '1cb8b73c74c47677b329fad4095fb9370227d4b8f1969372411478a4586c46cf'), +) + + +# Existing libelf 0.194-4 files in the inventoried runtime; reject other replacements. +REPLACES = {'usr/lib/aarch64-linux-gnu/libelf-0.194.so': 'ade23632ea253cb4ecfe95d5436bfeb3d40ae2003c5fe10bf1ac1c5b30bdf2e2', 'usr/share/doc/libelf1t64/changelog.Debian.gz': 'ffb0ea69fdbf6f32053059985048676931fd7d5c4458f8ca4bc54750cf4b1b31', 'usr/share/doc/libelf1t64/changelog.gz': '2e8e0af5c39ab744ac6934fe07419b5fd5504a50cd2f42110dd04315c548a708', 'usr/share/doc/libelf1t64/copyright': '2409860c86675ab33f8ad22480608faf849cbc2559d6478f708c23c508a1dbf2', 'usr/share/locale/de/LC_MESSAGES/elfutils.mo': 'f72328e130e63f231acb8e1a56ce5f4f3e91d0fc290db95d7ba4e4b2448d4d93', 'usr/share/locale/en@boldquot/LC_MESSAGES/elfutils.mo': '90158a24e3f85aaacc64085ef2644ba19075cb847c754341c9f245aaa69e94c6', 'usr/share/locale/en@quot/LC_MESSAGES/elfutils.mo': '33c6cda39c2145463dfdb2eb849922dbf6ccf51a9e4bfa8fd757744712d9abfa', 'usr/share/locale/es/LC_MESSAGES/elfutils.mo': '1ebff7e5583759b3f08eb5caaf18dd1a6ed6eb5ec71a02502ed8e21b134ad54f', 'usr/share/locale/ja/LC_MESSAGES/elfutils.mo': '764ed859f3aebd861da0b73c69da4d72d0f252952bb3be1c9c66360258a1661c', 'usr/share/locale/pl/LC_MESSAGES/elfutils.mo': '8abb000f43024994a9ec3032b52eb10858e004b81bc61c7ceea96f7467d13fdd', 'usr/share/locale/uk/LC_MESSAGES/elfutils.mo': 'ce0d747f478ee91a1cdef66bfd1e230a3ff1e787358f4c6e1ffb6513df713117'} + +def stage(root, cache=None): + cache = cache or Path(__file__).resolve().parents[1]/'downloads/native-packages-20261008' + cache.mkdir(parents=True, exist_ok=True) + records = [] + for name, directory, sha in PACKAGES: + url = 'https://deb.debian.org/debian/pool/main/'+directory+'/'+name + package = cache/name + if not package.exists(): + with urllib.request.urlopen(url, timeout=60) as response: + data = response.read() + if hashlib.sha256(data).hexdigest() != sha: + raise ValueError('Package checksum mismatch: '+name) + package.write_bytes(data) + if hashlib.sha256(package.read_bytes()).hexdigest() != sha: + raise ValueError('Package checksum mismatch: '+name) + members = subprocess.check_output(['ar', 't', str(package)], text=True).splitlines() + member = next(m for m in members if m.startswith('data.tar')) + payload = subprocess.check_output(['ar', 'p', str(package), member]) + with tempfile.TemporaryDirectory() as folder: + source = Path(folder) + with tarfile.open(fileobj=io.BytesIO(payload)) as archive: + archive.extractall(source, filter='data') + if name.startswith('ca-certificates'): + certificates = sorted((source/'usr/share/ca-certificates/mozilla').glob('*.crt')) + if len(certificates) < 50: + raise ValueError('Incomplete CA bundle') + target = root/'etc/ssl/certs/ca-certificates.crt' + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(b'\n'.join(p.read_bytes() for p in certificates)+b'\n') + target.chmod(0o644) + cert_link = root/'usr/lib/ssl/cert.pem' + cert_link.parent.mkdir(parents=True, exist_ok=True) + if not cert_link.exists() and not cert_link.is_symlink(): + cert_link.symlink_to('../../../etc/ssl/certs/ca-certificates.crt') + selected = [source/'usr/share/doc/ca-certificates/copyright'] + else: + selected = [p for p in (source/'usr').rglob('*') if p.is_file() or p.is_symlink()] + for path in selected: + target = root/path.relative_to(source) + target.parent.mkdir(parents=True, exist_ok=True) + if path.is_symlink(): + if target.exists() or target.is_symlink(): + if not target.is_symlink() or target.readlink() != path.readlink(): + raise ValueError('Conflicting package link: '+str(target)) + else: + target.symlink_to(path.readlink()) + else: + if target.exists() and target.read_bytes() != path.read_bytes(): + expected = REPLACES.get(str(path.relative_to(source))) + if not expected or hashlib.sha256(target.read_bytes()).hexdigest() != expected: + raise ValueError('Conflicting package file: '+str(target)) + shutil.copyfile(path, target) + target.chmod(path.stat().st_mode & 0o777) + records.append({'file': name, 'url': url, 'sha256': sha}) + record = root/'usr/share/vulpes/runtime-packages.json' + record.parent.mkdir(parents=True, exist_ok=True) + record.write_text(json.dumps(records, indent=2)+'\n') + return records diff --git a/tundra/runtime/setup-hal.py b/tundra/runtime/setup-hal.py index 7ee84bb2..c87b67c0 100644 --- a/tundra/runtime/setup-hal.py +++ b/tundra/runtime/setup-hal.py @@ -53,7 +53,19 @@ def main(): with image.open('rb') as f:sha=hashlib.file_digest(f,'sha256').hexdigest() if sha!='132d0e18c6a6a111293447e940267f35aa2b14fa00a872ef160b20dc2014d886':raise RuntimeError('Android reference changed') run('mount','-t','ext4','-o','loop,ro,noload',str(image),'/run/tundra/android-original') - run('mount','-t','ext4','-o','ro,noload','/dev/mmcblk0p70','/run/tundra/vendor-original') + hardware = Path('/etc/tundra/hardware-images.json') + bundled = json.loads(hardware.read_text())['images'] if hardware.exists() else None + if bundled: + if set(bundled) != {'vendor.img', 'modem.img'}: + raise RuntimeError('Expected both standalone hardware images') + for name, record in bundled.items(): + path = Path('/reference')/name + with path.open('rb') as source: + if path.stat().st_size != record['bytes'] or hashlib.file_digest(source, 'sha256').hexdigest() != record['sha256']: + raise RuntimeError('Standalone hardware image changed: '+name) + run('mount','-t','ext4','-o','loop,ro,noload','/reference/vendor.img','/run/tundra/vendor-original') + else: + run('mount','-t','ext4','-o','ro,noload','/dev/mmcblk0p70','/run/tundra/vendor-original') run('mount','-t','ext4','-o','ro,noload','/dev/mmcblk0p48','/mnt/vendor/persist') overlay('/run/tundra/android-original','/android','android') overlay('/run/tundra/vendor-original','/android/vendor','vendor') @@ -65,7 +77,10 @@ def main(): if count != 1:raise RuntimeError('Expected exactly one Sargo WLAN IPA setting') config.write_text(text) # vendor overlay upper is tmpfs, never the partition directory('/android/vendor/firmware_mnt') - run('mount','-t','vfat','-o','ro','/dev/mmcblk0p21','/android/vendor/firmware_mnt') + if bundled: + run('mount','-t','vfat','-o','loop,ro','/reference/modem.img','/android/vendor/firmware_mnt') + else: + run('mount','-t','vfat','-o','ro','/dev/mmcblk0p21','/android/vendor/firmware_mnt') report={} for p in sorted(Path('/android').rglob('*.rc')): if not p.is_file() or p.is_symlink() or not is_init_script(p):continue diff --git a/tundra/tests/test_hardware_status.py b/tundra/tests/test_hardware_status.py index 6a5745bc..190bcebe 100644 --- a/tundra/tests/test_hardware_status.py +++ b/tundra/tests/test_hardware_status.py @@ -50,7 +50,11 @@ class HardwareTests(unittest.TestCase): self.assertIn('unmanaged-devices=*,except:interface-name:wlan0', config) self.assertIn('auth-polkit=root-only', config) self.assertIn('enabled=false', config) - self.assertFalse((root/'etc/NetworkManager/system-connections').exists()) + connections = root/'etc/NetworkManager/system-connections' + self.assertTrue(connections.is_dir()) + self.assertEqual(list(connections.iterdir()), []) + wifi_service = (root/'etc/systemd/system/tundra-wifi.service').read_text() + self.assertIn('ReadWritePaths=/run /etc/NetworkManager/system-connections', wifi_service) self.assertIn('VULPES_TUNDRA=1', (root/'etc/systemd/system/tundra-display.service').read_text()) diff --git a/tundra/tests/test_runtime_packages.py b/tundra/tests/test_runtime_packages.py new file mode 100644 index 00000000..2f6531aa --- /dev/null +++ b/tundra/tests/test_runtime_packages.py @@ -0,0 +1,47 @@ +"""Check the pinned Bluetooth libraries and native TLS trust store offline.""" +import importlib.util +import json +from pathlib import Path +import ssl +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location('packages', ROOT/'runtime/runtime-packages.py') +packages = importlib.util.module_from_spec(spec) +spec.loader.exec_module(packages) +CACHE = ROOT/'downloads/native-packages-20261008' + + +class RuntimePackages(unittest.TestCase): + def test_corrupt_archive_is_rejected_before_extraction(self): + with tempfile.TemporaryDirectory() as folder: + base = Path(folder) + cache = base/'cache' + cache.mkdir() + (cache/packages.PACKAGES[0][0]).write_bytes(b'corrupt') + with self.assertRaisesRegex(ValueError, 'checksum mismatch'): + packages.stage(base/'root', cache) + self.assertFalse((base/'root').exists()) + + @unittest.skipUnless(all((CACHE/name).exists() for name, _, _ in packages.PACKAGES), + 'Pinned package cache is not installed') + def test_staged_payload_is_repeatable_and_certificates_are_usable(self): + with tempfile.TemporaryDirectory() as folder: + root = Path(folder) + first = packages.stage(root, CACHE) + self.assertEqual(packages.stage(root, CACHE), first) + self.assertEqual(json.loads((root/'usr/share/vulpes/runtime-packages.json').read_text()), first) + self.assertTrue((root/'usr/libexec/bluetooth/bluetoothd').is_file()) + context = ssl.create_default_context(cafile=str(root/'etc/ssl/certs/ca-certificates.crt')) + self.assertGreater(context.cert_store_stats()['x509_ca'], 50) + self.assertTrue((root/'usr/lib/ssl/cert.pem').is_file()) + target = root/'usr/libexec/bluetooth/bluetoothd' + target.write_bytes(b'unknown local binary') + with self.assertRaisesRegex(ValueError, 'Conflicting package file'): + packages.stage(root, CACHE) + self.assertEqual(target.read_bytes(), b'unknown local binary') + + +if __name__ == '__main__': + unittest.main() diff --git a/tundra/tests/test_sparse_backup.py b/tundra/tests/test_sparse_backup.py new file mode 100644 index 00000000..2305b36d --- /dev/null +++ b/tundra/tests/test_sparse_backup.py @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: MPL-2.0 +import io +from pathlib import Path +import struct +import sys +import unittest +sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools')) +from sparse_backup import receive + + +def record(offset,data):return struct.pack(' /newroot/reference/'+name, + 'mount --bind /volume/tundra/hardware/'+name+' /newroot/reference/'+name, + 'mount -o remount,bind,ro /newroot/reference/'+name] + config = json.dumps({'images':volume['hardwareImages']}, separators=(',',':')) + lines += ['mkdir -p /newroot/etc/tundra', + 'printf \'%s\\n\' '+shlex.quote(config)+' > /newroot/etc/tundra/hardware-images.json'] + native = native.replace(b'# Installed lower layers', ('\n'.join(lines)+'\n# Installed lower layers').encode()) + if not trial: + if b'/dev/mapper' in init or b'EXPECTED_STORAGE_HASH' in init: + # The old expected hash assignment is unnecessary in the raw image. + init = b'\n'.join(line for line in init.split(b'\n') if not line.startswith(b'EXPECTED_STORAGE_HASH=')) + if b'/dev/mapper' in init or b'/installed/' in native: + raise ValueError('Legacy storage leaked into standalone boot') + device = '/run/legacy-storage/tundra/standalone/'+volume['uuid']+'.ext4' if trial else '/dev/mmcblk0p72' + config = 'VOLUME_DEVICE='+shlex.quote(device)+'\nVOLUME_UUID='+shlex.quote(volume['uuid'])+'\nVOLUME_OPTIONS='+('loop,rw' if trial else 'rw')+'\n' + mount = (ROOT/'boot/standalone-mount.sh').read_bytes().replace(b'# VOLUME_CONFIGURATION', config.encode()) + return init, native, mount + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--volume', type=Path, required=True) + parser.add_argument('--prepared', type=Path, required=True) + parser.add_argument('--device-workspace', type=Path, required=True) + parser.add_argument('--output', type=Path, required=True) + parser.add_argument('--container-trial', action='store_true') + parser.add_argument('--no-deadline', action='store_true') + parser.add_argument('--boot-control', type=Path, help='Pinned ARM64 qbootctl binary') + parser.add_argument('--no-maintenance', action='store_true', help='Exclude private USB SSH credentials and server') + parser.add_argument('--local-debugger', action='store_true', help='Private RAM tests: loopback Marionette, never a public image') + args = parser.parse_args(); os.umask(0o077) + if args.local_debugger and args.no_maintenance: parser.error('Local debugger requires a private maintenance image') + volume = checked_volume(args.volume) + if args.no_deadline and not args.container_trial and not args.boot_control: + parser.error('Persistent standalone images require --boot-control') + if args.boot_control and digest(args.boot_control) != 'dcb00e4c4f038071a5d9979263128325e16b848903371f6c00d49cf3e25c34d9': + raise ValueError('Unqualified boot-control binary') + if args.boot_control and digest(args.boot_control.with_name('copyright')) != '8969e659aff7ed5bb3249341764c2bd048d5abe7400076f01485650e727e8c42': + raise ValueError('Boot-control license notice missing or changed') + system = json.loads((args.prepared/'system.json').read_text()) + state = json.loads((args.prepared/'data/state.json').read_text()) + if volume['systemImageSha256'] != system['imageSha256'] or volume['stateUuid'] != state['uuid']: + raise ValueError('Volume does not contain this system/data pair') + command = [sys.executable,str(ROOT/'tools/build-rescue-boot.py'), '--output',str(args.output), + '--device-workspace',str(args.device_workspace),'--layout',str(args.device_workspace/'layout.json'), + '--native','--runtime-config','--network','--hardware', + '--system-image',str(args.prepared/'system.json'),'--state',str(args.prepared/'data/state.json')] + if args.no_deadline: command.append('--no-deadline') + if args.local_debugger: command.append('--local-debugger') + subprocess.run(command,check=True) + out = args.output.resolve(); build = json.loads((out/'build.json').read_text()) + ramdisk = out/'initramfs.cpio.gz'; files = read_newc(ramdisk.read_bytes()) + init,native,mount = storage_scripts(files['init']['data'],files['native-root.sh']['data'],volume,args.container_trial) + if args.boot_control: + files['sbin/tundra-qbootctl']={'mode':stat.S_IFREG|0o755,'data':args.boot_control.read_bytes()} + files['qbootctl-copyright']={'mode':stat.S_IFREG|0o644,'data':args.boot_control.with_name('copyright').read_bytes()} + files['standalone-boot-success.py']={'mode':stat.S_IFREG|0o755,'data':(ROOT/'runtime/mark-boot-success.py').read_bytes()} + unit=b'[Unit]\nDescription=Acknowledge a healthy standalone Vulpes boot\nAfter=tundra-display.service\n[Service]\nType=oneshot\nTimeoutStartSec=650\nExecStart=/usr/bin/python3 /usr/libexec/tundra-mark-boot-success.py\n' + files['standalone-boot-success.service']={'mode':stat.S_IFREG|0o644,'data':unit} + setup=b"""mkdir -p /newroot/usr/libexec /newroot/etc/systemd/system/tundra.target.wants +mkdir -p /newroot/usr/share/doc/qbootctl +cp /qbootctl-copyright /newroot/usr/share/doc/qbootctl/copyright +cp /sbin/tundra-qbootctl /newroot/usr/libexec/tundra-qbootctl +cp /standalone-boot-success.py /newroot/usr/libexec/tundra-mark-boot-success.py +cp /standalone-boot-success.service /newroot/etc/systemd/system/tundra-boot-success.service +ln -sf ../tundra-boot-success.service /newroot/etc/systemd/system/tundra.target.wants/tundra-boot-success.service +""" + native=native.replace(b'# Installed lower layers',setup+b'# Installed lower layers') + if args.no_maintenance: + init=b'\n'.join(line for line in init.split(b'\n') if not line.startswith(b'/sbin/dropbear ')) + init=init.replace(b'TUNDRA_USB_SSH_READY',b'TUNDRA_USB_READY') + start=native.index(b'# Keep a minimal, already tested rescue environment') + end=native.index(b'for fs in dev proc sys run;',start) + native=native[:start]+b'# Public boot: no USB maintenance account or shared SSH key.\n'+native[end:] + for name in ('etc/tundra-hostkey','root/.ssh/authorized_keys','sbin/dropbear'): + files.pop(name,None) + files['init']['data']=init; files['native-root.sh']['data']=native + files['standalone-mount.sh']={'mode':stat.S_IFREG|0o755,'data':mount} + files['volume']={'mode':stat.S_IFDIR|0o755,'data':b''} + entries=[] + for name, item in sorted(files.items()): + entries.append((name,item['mode'],item['data'],5 if name=='dev/console' else 0,1 if name=='dev/console' else 0)) + ramdisk.write_bytes(cpio(entries)) + packager=checked_source('aosp-mkbootimg'); original=out/build['image'] + with tempfile.TemporaryDirectory() as temp: + unpack=Path(temp)/'unpack' + text=subprocess.check_output([sys.executable,str(packager/'unpack_bootimg.py'),'--boot_img',str(original),'--out',str(unpack),'--format=mkbootimg'],text=True) + options=shlex.split(text); fields=dict(zip(options[::2],options[1::2]));fields['--ramdisk']=str(ramdisk) + image=out/'boot.img'; command=[sys.executable,str(packager/'mkbootimg.py')] + for key,value in fields.items():command.extend([key,value]) + subprocess.run(command+['-o',str(image)],check=True) + verify=Path(temp)/'verify' + subprocess.run([sys.executable,str(packager/'unpack_bootimg.py'),'--boot_img',str(image),'--out',str(verify)],check=True,stdout=subprocess.DEVNULL) + if digest(verify/'kernel')!=build['kernelSha256'] or digest(verify/'ramdisk')!=digest(ramdisk): + raise ValueError('Standalone boot roundtrip failed') + original.unlink() + for path in (Path(__file__).resolve(),ROOT/'tools/standalone_volume.py',ROOT/'boot/standalone-mount.sh',ROOT/'runtime/mark-boot-success.py'): + relative=str(path.relative_to(ROOT)); target=out/'sources/tundra'/relative + target.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(path,target);build['sources'][relative]=digest(path) + build.update(image='boot.img',imageSha256=digest(image),ramdiskSha256=digest(ramdisk), + storageMode='container-test' if args.container_trial else 'raw-userdata-ext4', + standaloneVolume=volume,flashReady=False,bootControlSha256=digest(args.boot_control) if args.boot_control else None, + containsPrivateTrialHostKey=not args.no_maintenance,maintenanceEnabled=not args.no_maintenance, + scope='Standalone Tundra boot candidate; hardware qualification required') + (out/'build.json').write_text(json.dumps(build,indent=2)+'\n') + print(image) + + +if __name__=='__main__':main() diff --git a/tundra/tools/build-standalone-volume.py b/tundra/tools/build-standalone-volume.py new file mode 100644 index 00000000..bc2e28a7 --- /dev/null +++ b/tundra/tools/build-standalone-volume.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MPL-2.0 +"""Build a fresh ext4 userdata image. No connection to a phone or partition writes.""" +import argparse +import importlib.util +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import uuid +from persistent_state import FEATURES +from tundra import digest +from standalone_volume import checked_volume + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--prepared', required=True, type=Path) + parser.add_argument('--output', required=True, type=Path) + parser.add_argument('--hardware', type=Path, help='Locally supplied pinned vendor/modem images') + parser.add_argument('--size-mib', type=int, default=8192) + args = parser.parse_args() + if not 5120 <= args.size_mib <= 32768: + parser.error('Choose 5120–32768 MiB') + spec = importlib.util.spec_from_file_location('stage', Path(__file__).with_name('stage-sargo-files.py')) + stage = importlib.util.module_from_spec(spec); spec.loader.exec_module(stage) + files = stage.inputs(args.prepared) + hardware = {} + if args.hardware: + lock = json.loads((Path(__file__).resolve().parents[1]/'manifests/sargo-hardware.json').read_text()) + hardware = lock['images'] + for name, record in hardware.items(): + source = args.hardware/record['source'] + if source.is_symlink() or source.stat().st_size != record['bytes'] or digest(source) != record['sha256']: + raise ValueError('Hardware image changed: '+name) + payload_bytes = sum(p.stat().st_size for _,_,p,_ in files) + payload_bytes += sum(record['bytes'] for record in hardware.values()) + if args.size_mib*1024**2 < payload_bytes+512*1024**2: + raise ValueError('Insufficient space for payload and filesystem metadata') + os.umask(0o077) + out = args.output.resolve(); out.mkdir(parents=True, exist_ok=False) + (out/'.incomplete').touch() + seed = out/'seed'; seed.mkdir() + for kind, name, source, record in files: + target = seed/'tundra'/kind/name; target.parent.mkdir(parents=True, exist_ok=True) + # mke2fs reads the file; a hard link avoids another multi-gigabyte copy. + os.link(source, target) + if hardware: + directory = seed/'tundra/hardware'; directory.mkdir() + for name, record in hardware.items(): + os.link(args.hardware/record['source'], directory/name) + system = json.loads((args.prepared/'system.json').read_text()) + state = json.loads((args.prepared/'data/state.json').read_text()) + identifier = str(uuid.uuid4()) + (seed/'tundra-volume-id').write_text(identifier+'\n') + image = out/'userdata.ext4' + with image.open('xb') as f: f.truncate(args.size_mib*1024**2) + subprocess.run(['mke2fs','-q','-t','ext4','-F','-b','4096','-I','256','-O',FEATURES, + '-U',identifier,'-L','TUNDRA_VOLUME','-d',str(seed),str(image)],check=True) + names=['/tundra-volume-id','/tundra','/tundra/images','/tundra/data'] + names += ['/tundra/'+kind+'/'+name for kind,name,_,_ in files] + if hardware: + names += ['/tundra/hardware', *['/tundra/hardware/'+name for name in hardware]] + for name in names: + for field in ('uid','gid'): + subprocess.run(['debugfs','-w','-R',f'set_inode_field {name} {field} 0',str(image)],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL) + result = subprocess.run(['e2fsck','-f','-n',str(image)],capture_output=True,text=True) + (out/'filesystem-check.txt').write_text(result.stdout+result.stderr) + if result.returncode: raise ValueError('Standalone filesystem check failed') + manifest={'schema':1,'device':'sargo','storage':'raw-userdata-ext4','image':image.name, + 'uuid':identifier,'bytes':image.stat().st_size,'imageSha256':digest(image), + 'systemImageSha256':system['imageSha256'],'stateUuid':state['uuid'], + 'filesystemCheckPassed':True,'flashReady':False,'hardwareImages':hardware, + 'scope':'Fresh standalone storage; flashing userdata destroys previous user data'} + (out/'volume.json').write_text(json.dumps(manifest,indent=2)+'\n') + checked_volume(out/'volume.json') + (out/'.incomplete').unlink() + print(out/'volume.json') + + +if __name__ == '__main__': main() diff --git a/tundra/tools/build-system-image.py b/tundra/tools/build-system-image.py index 898afae2..fffd8014 100644 --- a/tundra/tools/build-system-image.py +++ b/tundra/tools/build-system-image.py @@ -128,7 +128,7 @@ def main(): ignore=shutil.ignore_patterns('__pycache__', 'test', 'tests')) for name in ('tools', 'tests', 'logs', 'profiles'): (product/name).mkdir(exist_ok=True) - for name in ('serve-gaia.py', 'project.py', 'run-host.py'): + for name in ('serve-gaia.py', 'project.py', 'run-host.py', 'installed_apps.py'): shutil.copy2(PROJECT/'tools'/name, product/'tools'/name) shutil.copy2(PROJECT/'tests/ProbeChild.sys.mjs', product/'tests/ProbeChild.sys.mjs') shutil.copy2(PROJECT/'release.json', product/'release.json') diff --git a/tundra/tools/flash-standalone-sargo.py b/tundra/tools/flash-standalone-sargo.py new file mode 100644 index 00000000..f7a01d1b --- /dev/null +++ b/tundra/tools/flash-standalone-sargo.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MPL-2.0 +"""Install a standalone Sargo package using Fastboot only (Python 3, any host OS). + +Default: verify files and display the destructive installation plan. +--install --erase-userdata replaces the old OS and ALL files in userdata. +Keep a separate backup. The unlocked bootloader and device calibration remain. +""" +import argparse +import hashlib +import json +from pathlib import Path +import re +import shutil +import struct +import subprocess +import sys + +PARTITIONS=('userdata','dtbo_a','vbmeta_a','boot_a') +NAMES={'userdata':'userdata.img','dtbo_a':'dtbo.img','vbmeta_a':'vbmeta.img','boot_a':'boot.img'} + + +def digest(path): + sha=hashlib.sha256() + with path.open('rb') as source: + while block:=source.read(1024*1024):sha.update(block) + return sha.hexdigest() + + +def sparse_size(path): + with path.open('rb') as source:header=source.read(28) + if len(header)!=28:raise ValueError('Truncated userdata image') + magic,major,minor,file_header,chunk_header,block_size,blocks,chunks,crc=struct.unpack('size:raise ValueError('Image exceeds partition: '+partition) + return info + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--package',type=Path,default=Path(__file__).resolve().parent) + parser.add_argument('--serial') + parser.add_argument('--install',action='store_true') + parser.add_argument('--erase-userdata',action='store_true') + parser.add_argument('--experimental',action='store_true') + parser.add_argument('--reboot',action='store_true') + args=parser.parse_args();value=package(args.package) + print('Pixel 3a / Sargo: replace userdata, dtbo_a, vbmeta_a and boot_a; activate slot A.') + print('ALL existing user data and the old OS in userdata will be lost. Bootloader remains unlocked.') + print('No existing Android or Droidian installation is required.') + if not args.install: + print('Review only; no device command sent.');return + if not args.erase_userdata:parser.error('--install requires --erase-userdata') + if not value.get('flashQualified') and not args.experimental: + parser.error('This candidate is not qualified; private tests require --experimental') + fastboot=shutil.which('fastboot') + if not fastboot:raise ValueError('Install Android platform-tools (fastboot) on the host computer') + devices=subprocess.check_output([fastboot,'devices'],text=True,timeout=10).splitlines() + serials=[line.split()[0] for line in devices if len(line.split())==2 and line.split()[1]=='fastboot'] + serial=args.serial + if serial is None: + if len(serials)!=1:raise ValueError('Connect exactly one Pixel or specify --serial') + serial=serials[0] + if serial not in serials:raise ValueError('Selected device not in Fastboot') + info=preflight(fastboot,serial,value) + report={'deviceSha256':hashlib.sha256(serial.encode()).hexdigest(),'preflight':info,'completed':[],'passed':False} + path=args.package/'install-report.json' + def save():path.write_text(json.dumps(report,indent=2)+'\n') + save() + subprocess.run([fastboot,'-s',serial,'erase','userdata'],check=True,timeout=300) + report['completed'].append('erase:userdata');save() + for partition in PARTITIONS: + subprocess.run([fastboot,'-s',serial,'flash',partition,str((args.package/NAMES[partition]).resolve())],check=True,timeout=1200) + report['completed'].append(partition);save() + subprocess.run([fastboot,'-s',serial,'set_active','a'],check=True,timeout=30) + report['passed']=True;save() + if args.reboot:subprocess.run([fastboot,'-s',serial,'reboot'],check=True,timeout=30) + print('Images written. Keep the bootloader unlocked. Select Start to launch Vulpes.') + + +if __name__=='__main__':main() diff --git a/tundra/tools/install-sargo.py b/tundra/tools/install-sargo.py index c626b64b..6263b261 100644 --- a/tundra/tools/install-sargo.py +++ b/tundra/tools/install-sargo.py @@ -23,6 +23,8 @@ def load(name, path): def checked_build(folder): folder=Path(folder).resolve() build=json.loads((folder/'build.json').read_text()) + if build.get('storageMode') in ('raw-userdata-ext4', 'container-test'): + raise ValueError('Use the standalone installer; the old boot-only plan is incompatible') if build.get('localDebugger'):raise ValueError('Diagnostic debugger image cannot be flashed') qualification=json.loads((folder/'qualification.json').read_text()) if (not qualification.get('passed') or not qualification.get('privateInstallReady') diff --git a/tundra/tools/package-standalone-sargo.py b/tundra/tools/package-standalone-sargo.py new file mode 100644 index 00000000..0390514b --- /dev/null +++ b/tundra/tools/package-standalone-sargo.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MPL-2.0 +"""Package boot, fresh userdata and boot metadata for a standalone installation. + +Builds local artifacts only. A candidate is never marked flash-qualified here. +Private development credentials, if present in boot, prohibit redistribution. +""" +import argparse +import importlib.util +import json +import os +from pathlib import Path +import shutil +import subprocess +from standalone_volume import checked_volume +from tundra import ROOT,digest + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--build',required=True,type=Path) + parser.add_argument('--volume',required=True,type=Path) + parser.add_argument('--dtbo',required=True,type=Path) + parser.add_argument('--output',required=True,type=Path) + args=parser.parse_args();os.umask(0o077) + volume=checked_volume(args.volume);build=json.loads((args.build/'build.json').read_text()) + if build.get('storageMode')!='raw-userdata-ext4' or build.get('watchdogSeconds') is not None or build.get('localDebugger'): + raise ValueError('Expected a persistent standalone boot without debugger or timed reboot') + if build['standaloneVolume']!=volume or not volume.get('hardwareImages'): + raise ValueError('Boot must match a volume with bundled vendor/modem images') + boot=args.build/build['image'] + if digest(boot)!=build['imageSha256']:raise ValueError('Boot image changed') + spec=importlib.util.spec_from_file_location('sources',ROOT/'tools/test-rescue-boot.py') + sources=importlib.util.module_from_spec(spec);spec.loader.exec_module(sources);sources.verify_sources(args.build,build) + lock=json.loads((ROOT/'manifests/sargo-hardware.json').read_text()) + if args.dtbo.stat().st_size!=lock['dtbo']['bytes'] or digest(args.dtbo)!=lock['dtbo']['sha256']: + raise ValueError('Unqualified device-tree overlay') + out=args.output.resolve();out.mkdir(parents=True,exist_ok=False) + (out/'.incomplete').touch() + shutil.copyfile(boot,out/'boot.img');shutil.copyfile(args.dtbo,out/'dtbo.img') + subprocess.run(['avbtool','make_vbmeta_image','--flags','3','--padding_size','4096','--algorithm','NONE','--output',str(out/'vbmeta.img')],check=True) + subprocess.run(['img2simg',str(args.volume.parent/volume['image']),str(out/'userdata.img')],check=True) + # Roundtrip verifies the actual sparse payload, not only its header. + restored=out/'userdata-roundtrip.ext4' + subprocess.run(['simg2img',str(out/'userdata.img'),str(restored)],check=True) + if digest(restored)!=volume['imageSha256']:raise ValueError('Sparse image roundtrip mismatch') + restored.unlink() + files={name:{'file':file,'bytes':(out/file).stat().st_size,'sha256':digest(out/file)} + for name,file in [('boot_a','boot.img'),('userdata','userdata.img'),('dtbo_a','dtbo.img'),('vbmeta_a','vbmeta.img')]} + value={'schema':1,'device':'sargo','storage':'raw-userdata-ext4','partitions':files, + 'userdataBytes':volume['bytes'],'userdataRawSha256':volume['imageSha256'], + 'volumeUuid':volume['uuid'],'systemImageSha256':volume['systemImageSha256'], + 'flashQualified':False,'containsPrivateMaintenanceKey':bool(build.get('containsPrivateTrialHostKey')), + 'redistributionReady':False,'previousOsRequired':False, + 'requirements':['Unlocked Pixel 3a (sargo)','Original device calibration partitions retained'], + 'scope':'Experimental standalone installation candidate; userdata is replaced in full'} + (out/'package.json').write_text(json.dumps(value,indent=2)+'\n') + shutil.copyfile(ROOT/'tools/flash-standalone-sargo.py',out/'flash-standalone-sargo.py') + (out/'SHA256SUMS').write_text(''.join(digest(p)+' '+p.name+'\n' for p in sorted(out.iterdir()) if p.is_file() and p.name not in ('.incomplete','SHA256SUMS'))) + (out/'.incomplete').unlink();print(out/'package.json') + + +if __name__=='__main__':main() diff --git a/tundra/tools/restore-standalone-sargo.py b/tundra/tools/restore-standalone-sargo.py new file mode 100644 index 00000000..e08f0b9e --- /dev/null +++ b/tundra/tools/restore-standalone-sargo.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MPL-2.0 +"""Restore a private full-storage backup after a standalone Sargo test. + +Default: verify the archive and partition backups only. --restore overwrites +userdata and the three slot-A boot partitions. Backups are device-specific. +""" +import argparse +import gzip +import hashlib +import json +from pathlib import Path +import shutil +import subprocess + + +def digest(path): + sha=hashlib.sha256() + with path.open('rb') as source: + while block:=source.read(4*1024**2):sha.update(block) + return sha.hexdigest() + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--backup',required=True,type=Path) + parser.add_argument('--restore',action='store_true') + args=parser.parse_args();folder=args.backup.resolve() + manifest=json.loads((folder/'recovery.json').read_text()) + if manifest.get('device')!='sargo' or manifest.get('activeSlot')!='a':raise ValueError('Wrong recovery target') + data=json.loads((folder/'userdata.json').read_text()) + if not data.get('checked') or data.get('archive')!='userdata-original.raw.gz':raise ValueError('Unverified userdata backup') + if not data.get('sourceWasUnmounted') or manifest.get('userdataSha256')!=data['sha256'] or manifest.get('userdataBytes')!=data['bytes']: + raise ValueError('Storage backup is not bound to this recovery kit') + for name,record in manifest['files'].items(): + if name not in ('boot_a.img','dtbo_a.img','vbmeta_a.img'):raise ValueError('Unexpected recovery partition') + path=folder/name + if path.is_symlink() or path.stat().st_size!=record['bytes'] or digest(path)!=record['sha256']:raise ValueError('Partition backup changed: '+name) + if set(manifest['files'])!={'boot_a.img','dtbo_a.img','vbmeta_a.img'}:raise ValueError('Incomplete boot backup set') + archive=folder/data['archive'];sha=hashlib.sha256();count=0 + with gzip.open(archive,'rb') as source: + while block:=source.read(4*1024**2): + count+=len(block) + if count>data['bytes']:raise ValueError('Oversized backup') + sha.update(block) + if count!=data['bytes'] or sha.hexdigest()!=data['sha256']:raise ValueError('Storage backup changed') + print('Private boot and full userdata backups verified.') + if not args.restore: + print('No phone action. --restore replaces its current userdata.');return + fastboot=shutil.which('fastboot') + if not fastboot:raise ValueError('Fastboot is required') + serials=[line.split()[0] for line in subprocess.check_output([fastboot,'devices'],text=True).splitlines() if len(line.split())==2] + matches=[serial for serial in serials if hashlib.sha256(serial.encode()).hexdigest()==manifest['serialSha256']] + if len(matches)!=1:raise ValueError('Original phone not present in Fastboot') + serial=matches[0] + # Use the standalone installer's strict Fastboot variable parser. + import importlib.util + spec=importlib.util.spec_from_file_location('flash',Path(__file__).with_name('flash-standalone-sargo.py')) + flash=importlib.util.module_from_spec(spec);spec.loader.exec_module(flash) + if flash.getvar(fastboot,serial,'product')!='sargo' or flash.getvar(fastboot,serial,'unlocked')!='yes':raise ValueError('Wrong or locked phone') + if int(flash.getvar(fastboot,serial,'partition-size:userdata'),16)!=data['bytes']:raise ValueError('Storage geometry changed') + raw=folder/'userdata-restore.raw' + if raw.exists(): + if raw.stat().st_size!=data['bytes'] or digest(raw)!=data['sha256']:raise ValueError('Existing restoration file changed') + else: + if shutil.disk_usage(folder).free65536 or offset+length>expected_size:raise ValueError('Invalid backup extent') + target.seek(offset);target.write(exact(stream,length));end=offset+length diff --git a/tundra/tools/standalone_volume.py b/tundra/tools/standalone_volume.py new file mode 100644 index 00000000..452f145b --- /dev/null +++ b/tundra/tools/standalone_volume.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: MPL-2.0 +"""Validate the fresh userdata filesystem used by standalone Sargo images.""" +import json +from pathlib import Path +import re +import uuid +from tundra import digest + + +def checked_volume(path, verify_image=True): + path = Path(path) + value = json.loads(path.read_text()) + if (value.get('schema') != 1 or value.get('device') != 'sargo' + or value.get('storage') != 'raw-userdata-ext4' + or value.get('image') != 'userdata.ext4' + or value.get('filesystemCheckPassed') is not True): + raise ValueError('Expected a checked standalone Sargo volume') + identifier = value.get('uuid') + if not isinstance(identifier, str) or str(uuid.UUID(identifier)) != identifier: + raise ValueError('Invalid volume UUID') + for key in ('imageSha256', 'systemImageSha256'): + if not re.fullmatch(r'[a-f0-9]{64}', value.get(key, '')): + raise ValueError('Invalid '+key) + if type(value.get('bytes')) is not int or not 5*1024**3 <= value['bytes'] <= 32*1024**3: + raise ValueError('Invalid standalone filesystem size') + hardware = value.get('hardwareImages', {}) + if hardware: + lock = json.loads((Path(__file__).resolve().parents[1]/'manifests/sargo-hardware.json').read_text()) + if hardware != lock['images']: + raise ValueError('Unqualified standalone hardware images') + if verify_image: + image = path.parent/value['image'] + if image.is_symlink() or image.stat().st_size != value['bytes'] or digest(image) != value['imageSha256']: + raise ValueError('Standalone volume changed') + return value From 402e5daec8195c0535998e9d1f5f4361b9007793 Mon Sep 17 00:00:00 2001 From: Alexandre NOEL <95a7002b-481b-4ef3-b578-f9c421550aa4@localhost> Date: Thu, 8 Oct 2026 15:27:22 +0200 Subject: [PATCH 02/12] docs: publish French and English changelogs on Forgejo --- CHANGELOG.en.md | 847 ++++++++++++++++++++++++++++++++++ CHANGELOG.md | 869 +++++++++++++++++++++++++++++++++++ README.fr.md | 2 +- README.md | 2 +- tools/prepare-publication.py | 4 +- 5 files changed, 1720 insertions(+), 4 deletions(-) create mode 100644 CHANGELOG.en.md create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md new file mode 100644 index 00000000..03b009cd --- /dev/null +++ b/CHANGELOG.en.md @@ -0,0 +1,847 @@ +# Changelog — Vulpes OS + +[Français](CHANGELOG.md) + +One Gaia base, three platforms: **desktop**, **Android (APK)** and **Tundra / Sargo**. +This journal covers visible changes and major technical milestones. + +**Status:** “In development” refers to local sources, “APK built” to a test package, +and “Published” to a public repository release. Shared source changes reach installed +APKs and phone images only after those packages are updated. + + + +## 8 October 2026 — Public source update + +### Shared · Desktop / Android / Tundra + +- Bring recent camera, Contacts, SMS and Marketplace fixes, plus Sargo build and recovery tools, into the public sources. Update both READMEs with verified capabilities and limitations. +- Make the Android parity check create its report directory in a fresh checkout. Verify that native Wi-Fi configuration ships an empty connections directory. +- Keep tickets, roadmaps, internal documents, personal data and keys outside the repository. + +### Packages and qualification + +- The public APK remains Preview17; the 8 October fixes require a future build. No new native image or flash in this step. +- At the latest Sargo check, cellular registration is still searching; two-way call audio, Bluetooth pairing and profiles remain to be qualified. + + + +## 8 October 2026 — Camera and Contacts workflows · in development + +### Shared · Gaia / Desktop / Android / Tundra + +- Serialize camera opening, await the previous sensor shutdown and release tracks and video resources after failures. Keep stable front/rear identities on desktop and Android. +- Preserve binary images across desktop/Tundra transport; explicitly encode contact photos and dates for Android's JSON bridge. +- Refresh contact details after editing. Restore Contacts → SMS, number picking and prefilled dialer activities, preserving nested requests and Gaia entry points. +- Reopen Gallery in picker mode when adding a contact photo, even when Gallery is already open for browsing. Selection, cropping and photo persistence verified on desktop. +- Add camera lifecycle, contact persistence, permissions, binary transport and activity routing tests. + +### Desktop + +- Verify the original Camera → JPEG capture → Gallery → photo opening → camera workflow. +- Verify Gaia contact creation, editing, detail refresh, accent-insensitive search and empty results, favourites, SMS recipient, number picker, prefilled keypad, cancelled deletion and confirmed deletion. The test sends no message and makes no call; its temporary contact is removed. + +### Tundra · Sargo + +- Verify on a RAM-booted Pixel 3a: autofocus, zoom, four front/rear switches with fresh frames, shutter capture, JPEG persistence, Gallery opening and sensor shutdown on Home. +- Check stored conversations, keyboard and notifications. Wi-Fi scan finds four networks. New SMS transmission/reception and call audio are still being qualified. +- Add SHA256-pinned BlueZ packages and required libraries. The controller starts, powers on and discovers; pairing through Gaia and Bluetooth profiles are not yet validated. +- Add native trusted certificates and verify HTTPS without disabling TLS. Test corrupted archives, file collisions and repeatable staging. + +- Include the corrected Contacts files in the boot build: contact creation, editing, SMS handoff, picking and dialer prefilling also pass using touch gestures on Sargo. + +### Android · APK + +- Publish the existing Preview17 APK and verify its download and SHA256. It predates the 8 October fixes. +- Fix recipient and body handoff from Gaia to the Android SMS app, including replies and a typed recipient that was not yet committed. Bridge tests pass; real Android validation is still required. +- Repackage corrected shared resources for the next build; no APK containing these new fixes has been published. + +**Status:** native qualification builds booted in RAM. No flashing, source push or private documentation publication in this step. Full phone qualification remains open. + + + +## 6 October 2026 — Embedded Marketplace theme · in development + +### Shared · Gaia / Desktop / Android / Tundra + +- Open the catalogue with `mode=app`, retained in navigation, search, filters, app details and language switches. +- Prepare a dedicated Vulpes web theme with local Fira fonts, compact navigation, touch-friendly controls, readable cards, collapsible filters and scrolling screenshots. Omit website headers in embedded mode while preserving the standard website layout. +- Keep confirmations and system messages in Gaia. The theme grants catalogue pages no additional privileges. +- Check 16 French/English pages, 2 standard pages and theme assets. Inspect mobile renders at 320 and 360 pixels. Desktop and Sargo launchers updated; shared sources will be included in the next Android packaging run. + +Website files prepared locally, not deployed to the public server. No push or new APK/ROM publication. + + + +## 6 October 2026 — Gaia banners · in development + +### Shared · Gaia / Desktop / Android / Tundra + +- Dismiss the “Application installed” banner after its normal timeout even if the animation-end event does not reach the Gaia component host. Keep its appearance and tap-to-dismiss behavior. +- Cancel old timers when a new message replaces the previous one, preventing premature dismissal. +- Check display, expiry, replacement and click dismissal on desktop; verify actual removal after five seconds on the Pixel. Fix deployed to the Sargo RAM trial and available in shared sources for the next APK. + +No flash or publication in this step. + + + +## 6 October 2026 — Wi-Fi and Settings panels · in development + +### Shared · Gaia / Desktop / Android / Tundra + +- Complete Settings panel navigation even when Gecko does not emit an animation-end event. Preserve Gaia transitions and prevent duplicate initialization. +- Updated shared sources and the desktop server; included in the next Android packaging run. No new APK published in this step. + +### Tundra · Sargo + +- Fix Wi-Fi signal events: include the associated network and do not emit connection information while disconnected. The incomplete event prevented Gaia from displaying scan results. +- Check the driver, native service, Gecko bridge and corrected panel on the Pixel: four access points detected, grouped into two networks, with no scan error. +- Wi-Fi connection confirmed by the user; a real HTTPS request from Gecko to example.org passed (HTTP 200, certificate verification enabled). Fixes applied to the RAM trial; no flash or Git push. + + + +## 6 October 2026 — Marketplace · in development + +### Desktop / Tundra · Sargo + +- Keep the catalogue below the status bar and inside its owning application. Hide its web view on Home and in the task switcher; remove it when the app closes. +- Prevent a cancelled navigation or a previous error page from hiding a retry. +- Show a local error message with a retry action when the catalogue is unavailable, instead of leaving its error page over Gaia. +- Desktop navigation, network failure, retry, Home and closing from the task switcher checked. Seven direct checks pass on Sargo in RAM without a debugger window, using a local page and a forced-unlock precondition; the remote catalogue requires an Internet connection on the phone. + +### Android · APK + +- Rebuild the local Preview17 APK with the shared Marketplace error message. Signature, alignment and asset parity checked; no new Android device regression run in this step. + +Nothing published in this step. + + + +## 2.7-preview.17 — APK built · 5 October 2026 + +### Shared · Gaia / Desktop + +- Restore Marketplace on Home with its original icon and the Vulpes catalogue. Select French for fr languages and English otherwise. +- Turn catalogue downloads into confirmed installations: validate ZIP archives, add the application to Home and support launch and removal. +- Store installed applications separately from Gaia sources, on distinct origins and without system privileges inherited from archived manifests. +- Validate the real desktop workflow with 2048: catalogue, confirmation, installation, launch, persistence and removal. Check damaged archives, invalid paths, limits, cancellation and single-use confirmations. + +### Android · APK + +- Build and sign Preview17 with the same Marketplace and installation service. Use the existing GeckoView browser for the catalogue and private Android storage for packages. +- Verify the Install click, confirmation, Home icon and 2048 launch without access to the Vulpes system bridge on an Android 16/API36 emulator. Also verify persistence after force-stop/relaunch and removal. + +### Tundra · Sargo + +- Prepare a local boot image containing Marketplace, the installer and the added-application server. RAM boot performed on the Pixel on 6 October: Home and Marketplace resources verified, without flashing. Nine of ten system checks pass; the overall Gaia check records a viewport change during unlocking. Package installation on Sargo still needs qualification. + +**Limitations:** initial support for packaged web applications. Old privileged APIs, hosted apps and discontinued services are not restored automatically. Nothing published to Forgejo or the website. + + + +## 2.7-preview.16 — APK built · 5 October 2026 + +### Shared · Gaia / Desktop + +- Restore Gaia wallpaper and sound pickers, return navigation to Settings and cancellation through Home. Share persisted image and sound transport between desktop and Android. +- Propagate language changes to Home without granting general settings read access. Check French/English labels and persistence. +- Restore horizontal page snapping and hide development apps from Home without deleting their sources. +- Use the supplied Vulpes logo in the splash and adapt the original sound-list components to the current engine. +- Verify two consecutive wallpaper changes, sound selection and restored Gaia panels on desktop. + +### Android · APK + +- Build and sign local Preview16; verify shared payload and version. Nothing published in this step. +- Validate Vulpes as the HOME launcher, restore Gaia locking after sleep and Android Back navigation from the lockscreen camera. +- Hide Android system bars in immersive mode while retaining temporary swipe access. +- Android 16/API36 emulator checks: locking, camera/Back, French/English, Home without test apps, wallpaper and sound. Wallpaper retained after force-stop/relaunch with no repeated changes over 15 seconds. Other Android devices not qualified in this step. + +### Tundra · Sargo + +- Include shared fixes and app policy in a new test boot image. The previous boot reapplied an old file snapshot, undoing hot updates. +- RAM boot on Pixel passed: six language/Home/picker checks and ten system checks. Image/sound picker results verified without replacing personal selections. Installed boot remains Preview15; no partition flashed in this step. + +**Limitations:** third-party FFAA installation remains open. The site depends on old mozApps APIs and a nomodule script; an installer with application and permission isolation still needs implementation. Selecting a Gaia sound does not change the global Android ringtone; actual call audio was not qualified here. + + + +## In development — 4 October 2026 — Standalone Sargo installation + +### Tundra · Sargo + +- Build a fresh ext4 volume directly for userdata, including the system, writable data and vendor/modem hardware images. Startup no longer looks for files in a Droidian installation; installation requires neither an Android session nor ADB. +- Python Fastboot installer checks the Sargo model, unlock state, partition sizes and checksums, then replaces userdata, dtbo_a, vbmeta_a and boot_a. The bootloader stays unlocked and device-specific calibration is retained. +- Full offline userdata backup, complete comparison against the phone and recovery archive verification before flashing. Backups remain private; an actual restoration was not performed in this step. +- Two successful boots from internal storage: eleven Gaia and ten native checks per boot, verified boot-image readback and retained settings. Slot A is marked successful only after validating Gaia startup in the current boot. +- Generic variant without an SSH server or maintenance keys: actual boot_a flash and boot passed. The development variant was then restored on the Pixel for further work. Generic package images retain their original fresh state. +- Final checks on the standalone development boot passed for Camera (including lens switches), the Gaia keyboard and notifications. +- Local Fastboot package assembled with checksums and a qualification report. 44 targeted tests pass. It is not published yet: provenance and redistribution of some hardware components still need final review. +- Limits: qualified on one Pixel 3a, not every firmware combination; an 8 GiB volume including 1 GiB of writable data, without automatic expansion. Hardware functionality remains experimental. + +### Desktop / Android · APK + +- No interface changes or new binary in this step. Gaia remains shared by all three platforms. + + + +## In development — 3 October 2026 — Per-device Sargo installation + +### Tundra · Sargo + +- Read-only collection of six boot partitions and existing storage, bound to the phone identity and slot A. Backups remain private. +- Portable device workspace: RAM builds, boot checks, installation and recovery export can use it without the development Pixel’s shared inventory. Workspace-bound builds reject implicit fallback to that inventory. +- Checked SSH transfers verify identity, storage and checksums and create files without replacing existing data. The transfer tool never formats, flashes or reboots. +- Build and transfer tested on the Pixel. First RAM boot passed eleven Gaia and ten native checks; keyboard and notifications passed. Camera testing encountered an autofocus timeout, retained in the report. +- Second boot of the same image retained the Gaia setting and passed eleven Gaia and ten native checks; camera, keyboard and notifications passed. The first autofocus timeout remains documented, not declared fixed. +- Verify return to the installed build: Gaia starts and the boot partition is unchanged. A repeated transfer correctly refuses to replace existing data. +- Refine HAL provenance: identify the installed package and download recipe and retrieve historical UBports build recipes. Exact correspondence with the old Android artifact and resolved source manifest remains unverified. +- Still limited to the qualified kernel and compatible ext4/LVM storage. This does not yet provide direct installation from stock Android. + +### Desktop / Android · APK + +- No interface changes or new build in this step. + + + +## In development — 3 October 2026 — Sargo preparation and recovery tools + +### Tundra · Sargo + +- Add local package preparation: verify manifest and archive checksums, bound decompression, check ext4 and create a fresh data image for each installation. Existing directories are never overwritten. +- Add a private recovery kit that can be exported outside the checkout. It retains the original boot_a backup and a standalone tool that checks model, serial number, slot and checksums before explicit restoration. It is not a user-data backup. +- Existing restoration no longer requires the diagnostic image to remain available. Backup validation is retained. +- System builds now require an explicit ARM64 hardware reference and reject display-only references or missing required services. +- Prepare the real system archive and verify the recovery kit locally. No new flash or public ROM release; automated installation on another Sargo still needs completion. + +### Shared · Desktop / Android / Tundra + +- Retain the shared JSON changelog in future source publications; internal Markdown working documents remain excluded. + +### Desktop / Android · APK + +- No functional changes or new build in this step. + + + +## In development — 3 October 2026 — Sargo modem recovery and fresh installation + +### Tundra · Sargo + +- Rebuild the system image with the font-cache fix and creation of oFono’s private state directory on first boot. +- Limit recovery to the modem subsystem: a controlled restart went offline, recovered and registered on the network in about 22 seconds without rebooting the phone. This does not fix the firmware’s IMS watchdog root cause. +- Check Gaia startup, hardware rendering, Fira fonts, both camera sensors, focus/zoom, keyboard and visual notifications with a fresh profile. Wi-Fi scanning still works after the modem restart. +- Two successful boots retained the same Gaia setting in a separate data image. Camera, keyboard and notifications rechecked after the second boot. Installed boot partition and existing data retained. +- No new native image published: the public installation/recovery package remains unfinished. + +### Desktop / Android · APK + +- No shared interface changes or new APK build in this step. + + + +## October 1, 2026 — Sargo distribution preparation — in development + +### Tundra · Sargo + +- Rebuild Preview 15 system image with empty profiles, internal working documents excluded, license notices retained and an inventory of 493 runtime packages. +- Filesystem and content checks completed; these checks do not qualify hardware boot. +- Read-only storage inventory and mount-table generation from that inventory instead of hardcoded development-phone dimensions. Limited to a single ext4 extent on Sargo userdata; no formatting added. +- Clean image transferred and verified on the charged Pixel. Temporary RAM boot tests left boot_a unchanged; automatic return to installed v59 and Gaia startup checked. +- SIM detection, LTE registration and Wi-Fi scanning checked on this image; these checks do not establish end-to-end calling, SMS or mobile data. +- Fix first-boot font cache ownership by running fc-cache as the session user. Gaia startup, hardware rendering, Fira, both camera sensors, keyboard and visual notifications checked in RAM. Qualification of the rebuilt image remains incomplete. +- Intermittent reboot traced to modem IMS firmware; targeted recovery policy applied and another boot with LTE, camera, keyboard and notifications passed. Recovery after a new IMS crash has not been exercised. The public image remains non-flashable and unpublished. + +### Desktop / APK + +- No functional changes or new APK build in this step. + + + +## 2.7-preview.15 — 30 September 2026 — published + +[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15) + +### Shared · Gaia / Desktop / Android / Tundra + +- First shared source publication, including English/French READMEs, licenses and setup scripts. Internal documents, roadmap, profiles, keys and private images excluded from the public repository. +- Include the latest SMS fixes (bubble dates, sent/failed states) and camera lifecycle fixes. The two 30 September entries detail the hardware changes. + +### Desktop PC + +- Published checkout tested with Gecko 156.0.1 and a fresh profile: Gaia startup, navigation, settings, gestures, history and page isolation. 42 service tests pass; service persistence after restart checked. +- Stabilize navigation checks during normal document replacement and allow more time for cold startup. Close the host cleanly even if Gaia exceeds its preparation timeout. + +### Android · APK + +- Preview 15 signed with the previous Preview key, GeckoView 156.0; shared resource parity checked. Installed as an update on an Android 16 emulator. +- Photo/Gallery capture, image display and sensor resume validated. Ten background/resume cycles and recovery after renderer termination validated. This does not qualify every Android phone. + +### Tundra · Sargo + +- Runtime sources included; private v59 remains the build qualified on the development phone. No generic flashable ROM published: boot still depends on inventoried storage, preinstalled images and private maintenance access. +- Limits remain: approximately 16 fps camera preview, incomplete flash/video, rotation and deep suspend; call audio awaiting qualification and a large dialog after SMS sending still under investigation. + + + +## In development — SMS and camera feedback · 30 September 2026 + +### Shared · Gaia / Desktop / Tundra + +- Messages: serialize bubble dates as numbers. Fix empty conversations caused by date formatting errors. Composer flow and sent/received bubbles verified on desktop with a simulated modem. +- Camera: make release idempotent; detach videos, await sensor release and dispose of the WebGL context before switching cameras. Identify each session so an old stream cannot stop its replacement. Repeated rear/front switches produce fresh frames; Home return and sensor shutdown passed on two RAM boots. + +### Tundra / Sargo + +- Vibration: use the DRV2624 brightness control; the legacy activate command did not physically stop the motor. The user confirmed stopping; single-pulse, 200/200/200 ms pattern and service-stop checks all returned to zero. Add a stop command when the service starts or exits. +- The user confirms focus, zoom and SMS transport both ways on v54. Five existing conversations render on the Pixel, with message bubbles and no date errors. +- v59 installed in boot_a after two successful RAM boots; installed startup and readback verified, slot A marked successful. Desktop sources and runtime synchronized. The large dialog after a real send still needs identification; camera smoothness remains separate work. + +### Android · APK + +- Shared Gaia sources updated; no APK rebuilt or published. + + + +## In development — Messages and Camera · 30 September 2026 + +### Desktop / Tundra + +- Messages now retain the message and conversation after a modem rejection and dispatch Gaia sending, failed and sent events. +- Distinguish the oFono queue from confirmed transmission; persist terminal states. Modem confirmation is not a recipient delivery receipt. +- Desktop checks cover failed messages visible in conversations, touch capture, Gallery opening and camera release/resume. + +### Tundra / Sargo + +- Connect touch focus areas, digital zoom and sensor JPEG capture to the original Gaia Camera. Preview stays at 640 × 480; directly measured JPEG sizes are 4032 × 3024 (12.19 MP) rear and 3264 × 2448 (7.99 MP) front. The front camera has fixed focus. +- Keep HAL logs out of the binary protocol, bound replies and resume preview after capture. A focus failure must not close the session. +- Reject SMS submission explicitly when unregistered; retain sent/failed states after the oFono object disappears. Carrier messages triggered Gaia notifications on v49 after LTE registration recovered. +- Qualification: two v54 RAM boots and the installed boot_a startup passed; image installed on the Pixel and slot marked successful after readback. Persistence, Gaia startup and Camera checks passed. Network registration remains unstable: SMS tests stopped before transmission, with no end-to-end delivery confirmed. + +### Android · APK + +- No APK rebuilt or published in this pass. The available Preview 14 is unchanged. + + + +## 2.7-preview.14 — APK published · 30 September 2026 + +[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.14) + +The Preview 14 APK is published. Desktop is up to date and the private Sargo v48 build is installed and tested; this release does not include a public phone image. + +### Shared · PC / APK / Gaia + +- About: remove the extra indentation in version rows above “Last updated”; alignment checked on desktop and Android. +- Fonts: normalize text font names inside Shadow DOM stylesheets, including styles added later; keep icon fonts intact. +- Calendar: month view opens without JavaScript errors in the desktop check. + +### PC / Tundra + +- Keyboard: use the screen orientation instead of the short keyboard frame. Keys measure about 60 CSS pixels on Pixel; rotation tested on desktop at DPR 3. +- SMS: restore the caret to the focused field when Gecko retains the recipient selection; preserve valid selections during editing. +- Gaia notifications: respect silent, no-sound and no-vibration flags, including restored notifications. + +- Gaia keyboard: native input bridge, numeric SMS recipient layout, corrected selection events and protection against stale field replies. Recipient and message typing tested on desktop and Pixel; carrier delivery still requires qualification. +- Lockscreen: tolerate the absence of a keyboard window during resize. +- Native notifications: persistent application-owned entries and launch through Gaia's declared Phone entry point. Tray and lockscreen display/removal verified on Sargo; audible sound still needs qualification. + +### Tundra / Sargo — v48 build installed + +- Vibration: Sargo actuator bridge, patterns limited to ten seconds, non-blocking sequencing and cancellation. Driver activation and stop measured. + +- Wi-Fi: private persistent profiles, autoconnect and forget operation; storage and failed-update rollback tests. Connection and reconnection after a RAM reboot verified on Sargo; private profile files (0600). +- Incoming SMS: persist before acknowledgement and deduplicate retries. Services now create their state directory at startup, fixing a mount failure reproduced on Sargo. +- Call audio: earpiece/speaker, microphone mute and call volume controls added; earpiece/speaker switching, mute and volume verified without placing a call. Two-way conversation still unverified. +- Local-debugger images cannot be flashed by the installer. v48/preview.14 written to boot_a after two RAM boots and persistence checks; installed boot verified, image readback matches and slot A marked successful. v43–v48 do not enable a debugger. + +### Android · APK + +- Preview.14 APK built, signed and published on Forgejo. Startup, version information, alignment and component font selection checked on an Android 16 emulator. Android retains its system keyboard. + + + +## 2.7-preview.13 — application workflows · 27 September 2026 + +### Shared · Gaia / Desktop + +- Calendar: successfully scheduling a reminder no longer rejects event saving; scheduling failures reach the caller. +- Reminders: ignore deleted events and do not mark denied notifications as delivered. Native Tundra notification support remains incomplete. +- Notifications: accept empty raw text without requesting an “undefined” translation. +- Gallery: positive thumbnail size even when starting hidden; cancel preview updates after leaving the editor. +- SMS: apply correspondent, read state, date range and delivery filters; cover conversation reading and deletion. +- Deeper review: WebM playback, seeking and deletion; black-and-white photo editing and saved copy; event creation and deletion. Retain original Gaia interfaces. + +- Isolated checkout qualification passes startup, desktop tests, persistence and Android packaging without rewriting sources. + +### Android · APK + +- Signed preview.13 APK built; Camera/Gallery, Calendar, ten background/resume cycles and renderer recovery validated on an Android 16 emulator. + +### Tundra · Sargo + +- Build v40/preview.13 installed after two RAM boots: Gaia, camera, Wi-Fi, brightness and controls checked; matching boot_a readback and successful slot marking. Remaining limits in `docs/APPLICATION-REVIEW.md`. + + + +## 2.7-preview.12 — application review · 27 September 2026 + +### Shared · Gaia / Desktop + +- Restore original Gaia 1×–2.25× raster variants for 1,047 CSS image references. +- Camera switch becomes available after device discovery; unavailable flash stays + visible and disabled; pinching no longer zooms the whole camera application. +- Reserve Home gestures for long press and suppress its context menu; retain Gaia's task manager. +- Relay Gaia touch gestures using unprivileged DOM events, without granting trusted + user activation to external websites. +- Bundle the supplied bilingual, local browser welcome page and route links and + searches into the actual browser. +- Welcome page follows the Gaia language live: `fr`/`fr-*` select French; + all other languages select English. +- SMS: numeric identifiers, migration of older records, Date values, sent events, + multi-recipient request shape and serialized storage writes. +- Calendar: restore the current day's accessibility label in 37 locales. +- Review 14 apps; 26 media/clock/UI checks, contact and SMS checks, plus real Web + navigation and isolation. Scope and limitations: `docs/APPLICATION-REVIEW.md`. + +### Android · APK + +- Rebuild `2.7-preview.12` with the same shared UI and local welcome page; check + payload parity. Android camera capture continues to use getUserMedia. + +- Tundra hardware features are not automatically ported to Android: each platform + retains its adapter and reports its actual capabilities. + +### Tundra · Sargo + +- Convert NV21 using a shader and transfer binary frames instead of base64: + approximately 23 fps measured in RAM, versus 8 fps in the intermediate trial. +- Wait for camera worker exit before opening the other sensor. +- Manual brightness reaches its target in about 200 ms in the hardware test; + keep screen sleep/wake fades. +- Connect Gaia Wi-Fi power settings to actual radio state; support WPA-PSK on + mixed WPA2/WPA3 networks. +- Scan, connection and HTTPS over wlan0 validated in RAM; connection profiles are temporary. +- Wi-Fi profiles are still temporary. Flash, video, light/orientation sensors, + Bluetooth, data counters and advanced call controls remain incomplete. +- v38 installed: two RAM boots, installed startup, `boot_a` readback, other boot + partitions preserved and slot A marked successful. + + + +## In development — Sargo feedback and interface · 27 September 2026 + +### Shared · Gaia / Desktop + +- Shutdown animation: three blue shades replace the orange rings; original Gaia + dimensions, sequence and timing are preserved. + +- Phone: prevent document scrolling on tab changes, keeping Recents and Contacts + headers in place. Use Gaia’s original call-log artwork at multiple densities up to 2.25×. +- Select controls: remove the old engine offset that clipped Fira descenders. + Video: keep the introductory text within the viewport. +- Brightness: save a number from input/change events; normalize legacy text values + before Gaia animates the transition. Coalesce stale hardware brightness steps + while preserving the order of sleep/wake commands. +- Home: extend wallpaper beneath the transparent gradient, reserve navigation space + inside applications, and place Home on the right in landscape. +- Browser: editable local page at `overrides/search.gaiamobile.org/home/index.html`, + retaining Gaia integration and private mode. +- Wi-Fi: show a scan failure and retry action instead of endless retries. Network + discovery and results in the original Gaia panel now verified on Sargo in RAM. +- Consolidated tracking: `docs/CHANTIER.md` includes earlier and new feedback and limitations. + +### Android · APK + +- Build and sign `2.7-preview.11` with shared Gaia; payload parity checked. + Place Home on the right in landscape and fix its wallpaper background. + Check rotation and non-overlapping app/navigation bounds on Android 16 emulator. + +### Tundra · Sargo + +- Camera: hardware preview in Gaia, JPEG capture and rear/front switching verified + on Sargo in RAM; sensors close on returning Home. Capture is currently limited + to 640 × 480 (480 × 640 portrait); video and flash remain unavailable. + Desktop and APK keep getUserMedia and share the same Gaia interface. + +- v33 (`2.7-preview.11`) installed after two successful RAM boots. Installed boot, `boot_a` readback, + data preservation and slot A success finalization verified. +- Backlight measured from Gaia: 35% → 89/255, 80% → 204/255. Wi-Fi results visible + in the original panel. Blue shutdown rings included. +- Call audio: support PulseAudio 14 text output and prevent loading audio modules twice. + Voice-call profile, earpiece selection and media-profile restoration verified on Sargo. + An actual two-way conversation remains unvalidated. +- Still open: audible two-way calls, full-resolution camera capture/video and sensor-driven rotation. + See the tracking document. + + + +## In development — native Wi-Fi stability · 27 September 2026 + +### Shared · Gaia / Desktop + +- Fix the Wi-Fi loop with no delay: scan every 20 seconds as intended by Gaia. + Coalesce concurrent scans. Add burst, failure and timer regression tests. +- Shared development version: `2.7-preview.9`, Gaia `2.7.0`; actual Gecko versions + remain unchanged. + +### Android · APK + +- Build `2.7-preview.9` to retain shared Gaia parity. Android still manages its + own Wi-Fi; this change does not claim to add a hardware driver to the APK. + +### Tundra · Sargo + +- Identify the v21/v22 failure: unbounded Python Wi-Fi scan clients exhausted + memory, causing the lowmemorykiller to terminate Gecko. The phoc SIGSEGV occurs + afterwards during display teardown. +- Allow one hardware scan at a time, briefly cache results and errors, invalidate + after connect/disconnect. Application isolation and memory protection remain enabled. +- Qualify v25 through two RAM boots, then install and verify it on the Pixel: + 11 Gaia and 10 system checks pass, with no memory-pressure process kills. + Check the Gaia lockscreen, Fira, battery, date and Wi-Fi panel; exercise + sleep/wake and volume through injected input events. +- Verify `boot_a` by readback, preserve the other boot partitions and user data, + and mark slot A successful. Native camera support and full call validation + remain unfinished. + + + +## In development — Gaia restoration · 27 September 2026 + +### Shared · Gaia / Desktop + +- Restore Gaia panels and remove temporary windows/floating controls: Settings, + Usage, FM and contact/media import. Features without an API remain visible with + disabled controls, without invented hardware values. +- Fix legacy style scoping: ordinary `div` selectors no longer become component + roots. Restore lockscreen icons, retain Fira and use scalable Gaia glyphs for + Phone tabs. +- Reserve 44 CSS pixels for Home so it cannot cover Answer/Hang up. The date uses + its original component; battery updates reach Settings and its labels no longer + display `{{ level }}`. +- Re-enable the original Gaia lockscreen; temporary splash uses the supplied logo, + stable dimensions and a gentle animation. Localized name: Navigateur / Browser. + Credits appear in Legal Information; original license notices remain. +- All 26 application checks pass (Gallery, Music, Clock, task management). Further + desktop checks cover panels, imports, battery, date and navigation; simulated + calls, history, sleep/wake, Power menu and volume. See `docs/GAIA-RESTORATION.md`. + +### Android · APK + +- Build and install `2.7-preview.8` on the Android 16 emulator with shared Gaia; read battery state from Android, + add Android Settings at the bottom and optional Android Home mode. The Android + application catalogue in Gaia still needs integration. +- Android checks: ten background/resume cycles and renderer termination recovery; + shared payload parity verified. No physical Android device test in this pass. +- Imports use Gaia components; the SMS button opens Android confirmation. + The internal lockscreen does not replace Android security. + +### Tundra · Sargo + +- Build v21/v22 candidates with all `overrides/` fixes in the boot payload; connect Wi-Fi + scan/association to the Gaia panel through NetworkManager. +- RAM trials blocked: the display service exits with code 139 before qualification. + No new flash. Restore installed v20 and verify 11 Gaia and 9 system checks with + preserved data; restore the native profile’s previous unlocked state. Candidate + Wi-Fi, native camera, full SIM/Bluetooth APIs and CPU suspend remain unqualified. + + + +## In development — 27 September 2026 + +### Shared · Gaia / Desktop + +- Replace the temporary call overlay with Gaia’s original **Callscreen**: + incoming/outgoing calls, answer, hang up and automatic close. A compatibility + adapter supplies `mozTelephony` events to the original modules. Unsupported + audio/conference controls remain disabled. +- Persist completed calls in the host and import them into Gaia’s original call + log, with acknowledgement and duplicate detection after interruption. +- Connect real battery state to the original status icon; unknown is not empty. + Handle a missing/malformed Facebook contacts index without blocking local + number lookup or call history. +- Compare rendered font faces: home already used Fira on both platforms, but + desktop system text partly used Noto. Main text rules now select Gaia’s + bundled Fira face while preserving icon fonts. +- Desktop Gecko test with a simulated modem covers incoming/outgoing UI, + answer/hang up, history, screen sleep/wake, power menu, volume and battery. + No APK rebuilt in this step. + +- Earlier v13 step (superseded above): system-owned call screen with dialing, ringing, connected state and Hang up. + Controls remain available if the service temporarily loses call status. +- Tested in desktop Gecko with a simulated modem, including hang-up failure and + retry. Desktop sources synchronized; no APK rebuilt. + +### Tundra · Sargo + +- Private v20 installed and read back in `boot_a` after two RAM boots and one + installed boot; settings retained and slot marked successful. +- Read only Sargo’s physical Power/Volume keys and forward + their edges to Gaia HardwareButtons. Short press sleeps/wakes the screen; + long press opens Gaia’s menu. The restricted native broker controls backlight, + volume, shutdown and reboot. +- Serialized brightness and power changes: concurrent requests no longer + leave the backlight disabled while waking the screen. +- Native volume: expose the session PulseAudio socket read-only to the service; + otherwise Gaia’s indicator changed while the audio output remained unchanged. +- Screen sleep does not suspend the kernel yet. Real incoming calls, conversation + audio and advanced call audio controls remain to be qualified. + +- Earlier step: private v13 installed in `boot_a` after two RAM boots; installed boot, + readback and the A/B success flag verified. +- oFono call state reporting and Answer/Hang up commands restricted to modem call paths. +- Successful void D-Bus responses no longer cause a false invalid-response error. +- A short real outgoing call reached ringing and was automatically hung up. + Conversation audio and incoming calls remain unqualified. +- Camera diagnostics no longer mistake a timeout for successful initialization. + Libhybris detects two sensors, but the preview probe receives no frames. + Native Gaia camera access remains unavailable. + + + +## In development — 26 September 2026 + +### Shared · Gaia + +- Fonts: use the archived simulator’s Fira Sans OpenType files for both Gaia + pages and Tundra system text. Fix native sans-serif fallback and add Fontconfig + validation; icon fonts remain separate. +- Settings: remove the “Tundra settings” and “Test a notification” diagnostic + buttons. Release information and the technical details remain available. +- Hardware panel: pass platform capabilities into the panel. An undefined variable + used to open a Gecko alert instead of displaying battery information. + +### Desktop + +- Local adapter synchronized with the shared sources. Gecko remains at 156.0.1. + +### Android + +- Hardware panel fix is in the shared sources; no new APK has been built. + +### Tundra / Sargo + +- Explicit A/B boot-success finalization after installation: backed-up and checked + GPT metadata, verified Qualcomm utility, Fastboot confirmation. +- Modem rejections distinguished from transport failures; errors shown inside Gaia. + The unavailable native camera no longer suggests an ineffective permission change. +- Rendered-font and radio diagnostics without automatic calls or messages. Fira Sans + confirmed on the home screen and status bar. +- Installed the v9 font build into `boot_a` on the inventoried Sargo. Written + image verified by readback; the other five backed-up boot partitions are unchanged. +- Two boots from device storage pass 10 Gaia and 9 system checks each and retain + settings. A PC is no longer required to boot. Droidian fallback files retained; + actual restoration has not been tested. +- Private persistent candidate: separate 2 GiB ext4 data image, retained Gaia + settings/profiles and system identities, no ten-minute reboot watchdog. +- Two consecutive boots of the same v8 image: 9 Gaia and 9 system checks pass on + each boot. Separate diagnostic: 15 checks using identical shared UI files. +- Boot-a-only installation and original-boot restoration tools prepared and tested + locally. No actual flash performed; remaining hardware limits are documented. +- Normal startup no longer opens Settings/Clock or scrolls the home screen. These + actions now belong to the explicit `--diagnostics` mode. +- Enter fullscreen before loading Gaia and hide native window decorations. On the + tested Pixel: 360 × 740 CSS pixels at scale 3, stable dimensions and an unchanged + home screen throughout five post-startup samples. +- v13 RAM trial passes eight startup checks and nine system checks. Reports now + distinguish startup observation from interactive qualification. +- Separate v14 diagnostic: thirteen interactive checks pass, including the hardware + panel; Wi-Fi panel and scanning verified with the same UI files as normal v13. +- Interactive diagnostic images are rejected as the normal launch target. The + earlier v13 selection is retained in the launcher history. +- Modem registered on UMTS during the trial; audio playback completed and microphone + samples were measured without retaining a recording. In-call audio, camera and + suspend remain unqualified. +- Ramdisk dependency discovery now runs `readelf` in the C locale, preserving the + ARM64 dynamic loader regardless of the build machine's language. + +All six A/B boot/DTBO/vbmeta partitions are unchanged. This remains a private RAM +trial, **not a flashable ROM**. See [Tundra status](tundra/docs/STATUS.md). + + + +## In development — 25 September 2026 + +### Shared · Gaia + +- **Navigation:** a circular Home button, a black-to-transparent gradient over the wallpaper, + and a solid blue bar inside apps. Pressing Home now changes the ring instead of leaving + a blue rectangle over the home screen. +- **Scrolling:** the gradient disappears at the bottom of the home panel and returns when + scrolling up. Switching between the Apps and Pages panels is supported. +- **Typography:** bundled Fira Sans fonts, with multiple weights and italics, instead of + relying on the host system’s installed fonts. +- **Device information:** separate product, Gaia, actual engine, platform, model and adapter + versions. The panel includes support for Tundra and hardware adaptation fields; + connecting these to the native platform is still pending. +- **Icons:** loading resumes after an initial zero-width measurement, across desktop, + Android and Tundra. This fix was previously limited to Android. +- **Maintenance:** this changelog brings together the three platforms’ progress. + +### Desktop · Locally tested + +- Tested an icon initially measured at zero width, then made visible. +- Tested Home / Settings / Home navigation, a transparent bar at the bottom, the gradient + at the top and permission checks in an isolated profile. +- Tested the information panel in French and English, without duplicate legacy rows. +- The engine remains Gecko **156.0.1**; these UI changes do not rebuild Gecko. + +### Android · Sources adapted, Java compiled + +- The GeckoView / Android bridge implements the same navigation states: an overlay on Home, + with space reserved for the bar in apps and the browser. +- Navigation state survives Android activity recreation. +- Java compilation passed. **No new APK delivered for these changes**; + visual behaviour still needs testing on Android. + +### Tundra / Sargo · Private RAM trials + +- Network build v6 is the launcher reference after 13 Gaia and 9 system checks; + automatic return and unchanged hashes of all six boot partitions verified. +- Separate Tundra backend reports Pixel 3a, Sargo adaptation and native version, + without claiming an unimplemented camera or telephony stack. +- Real battery and charging state with a bilingual Gaia hardware panel. + The network trial passed 13 Gaia checks, including this panel. +- Real Wi-Fi tested: scan, connection, IPv4, DNS and HTTPS in Gecko. Added + `pd-mapper`, disabled uninitialized IPA offload and premature camera probing; + 9 system checks pass without kernel Oops. Gaia network selection is still + pending; trial credentials remain in RAM. +- Service configuration can travel in the small ramdisk, avoiding another 3 GiB + transfer per boot adjustment. Qualcomm trials use RAM copies of modem NV data. + +- A new ext4 system image contains the ARM64 runtime, reference HALs, Phoc, Gecko and Gaia. + The runtime is no longer loaded from the Droidian working directory, nor the HAL + from its previous `/var/lib/lxc/android/` location. +- The second Pixel 3a trial passed **7 Gaia checks and 8 system checks**, with Tundra PID 1, + its HAL services, compositor and Gaia. +- A physical display capture confirms home icons, loaded Fira Sans, the circular button + and gradient. Gecko **156.0.1 ARM64**, with hardware WebRender. +- Small UI changes travel in the trial ramdisk, avoiding another 3 GiB system transfer + for each CSS or JavaScript adjustment. +- Build sources are saved alongside new images and verified by hashes, so qualified + images remain testable after the checkout changes. +- The latest trial (`sargo-system-boot-20260925-v5`) passes **10 Gaia checks and 8 system + checks**, including icons, fonts, pressed Home, bottom-of-page behaviour and app return. + Physical capture reviewed; local launcher reference updated. No APK published or phone flashed. + +**Limitations:** the installed filesystem still carries the system image; writes are lost +at shutdown; the trial returns to the retained system after ten minutes. No flashing or +Droidian replacement. A warm compositor restart failed during one trial; cold boot works. +Networking, modem, audio, camera and suspend still need integration or qualification. +This is not a public installable ROM or a validated daily-use phone. + + + +## 2.7-preview.7 — APK built, local reference + +### Shared / Desktop / Android + +- Fixed internal camera allocation permission on desktop and recovery after the first + CAMERA permission prompt on Android. +- Photo distinguishes permission denial, a missing sensor and an opening failure. +- Shared product version **2.7-preview.7**, Gaia **2.7.0**, with separate engine versions. +- Desktop Gecko **156.0.1**, Android GeckoView **156.0**: the real patch difference is + reported rather than relabelling either engine. + + + +## 2.7-preview.6 — APK built + +### Shared / Desktop / Android + +- Ported the original Gaia Camera app: webcam preview and JPEG capture on desktop, + or capture through GeckoView on Android. +- Photos saved in Gaia Gallery; media imports use the host system’s file picker. +- Unsupported controls disabled, including flash and video; camera switching depends + on available sensors. + + + +## 2.7-preview.5 — APK built + +### Shared + +- Centralised versions in `release.json`, separating product, Gaia and engine versions. +- Shared native integration UI between desktop and Android; packaged common files + checked inside the APK before distribution. + +### Desktop + +- Integration with available Linux settings panels, media and vCard imports, + desktop notifications and a test webcam capture. + +### Android + +- Same Gaia interface and native commands as desktop, adapted to Android capabilities. +- Foreground return and recovery after rendering-process termination tested. + + + +## 2.7-156.0.2 — Historical APK built (revision 4) + +This historical name mixed product revision and engine version; it does not mean +that the APK used Gecko 156.0.2. Its Android engine was Gecko 156.0. + +### Android + +- Activity and session recovery to reduce white screens when returning to Vulpes. +- Access to Android settings, volume and window brightness. +- Media and contact imports, native notifications with permissions. +- Handoff to Android’s dialer, SMS and clock apps; no silent sending or replacement + of the system Phone and SMS apps. +- Capture initially delegated to Android’s camera app, before the Gaia Camera port. + + + +## 2.7-156.0.1 — APK published (revision 3) + +- **Vulpes OS - Preview** name and project icon. +- Wallpaper flickering fix included. +- Historical numbering retained; it does not identify the Android engine’s patch version. + +[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1) + + + +## 2.7-preview.2 — APK published + +- Fixed wallpaper flickering in the Android Preview. + +[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.2) + + + +## 2.7-preview.1 — APK published + +- First Android Preview bundling Gaia and GeckoView. +- Archived version; known wallpaper flickering issue. + +[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.1) + + + +## Earlier milestones — technical retrospective + +These milestones are not newly dated public releases. + +### Gaia / Desktop + +- Vulpes assembled from historical Gaia: branding, wallpaper, translations and + work on system information. +- Experimental Gecko 45 → 52 port, followed by separation of Gaia, services and + the engine for the current Gecko host. +- Reimplementation of required APIs: settings, application registry, local contacts, + messages, alarms, storage and inter-app communication. +- Adaptation of old UI components, simulated touch, browser and applications; + testing of archived Firefox OS apps. +- Gecko update tools with checks, candidate engines and explicit rollback. + They detect incompatibilities without automatically repairing them. +- Gaia launch and updates without rebuilding the whole engine. + +### Tundra / Sargo + +- Pixel 3a inventory, boot partition backups and USB diagnostic trials. +- QEMU trials, then ARM64 Gecko inside Droidian and under a dedicated compositor. +- Separate root, selected HAL services and Tundra PID 1; temporary graphical + Fastboot trials with rescue SSH and automatic return. +- Reference kernel **4.9-124-google-sargo**, **Android 9 / API 28** adaptation. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..cab05b3a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,869 @@ +# Journal des évolutions — Vulpes OS + +[English](CHANGELOG.en.md) + +Une base Gaia, trois plateformes : **PC**, **Android (APK)** et **Tundra / Sargo**. +Ce journal décrit les changements visibles et les étapes techniques importantes. +Les rapports détaillés restent dans la documentation de chaque plateforme. + +**Statuts :** « En développement » concerne les sources locales, « APK construit » +un paquet de test, et « Publié » une release accessible dans le dépôt. Un correctif +commun n’apparaît dans un APK installé ou une image téléphone qu’après leur mise à jour. + + + +## 8 octobre 2026 — Mise à jour des sources publiques + +### Commun · PC / Android / Tundra + +- Regrouper les correctifs récents de caméra, Contacts, SMS, Marketplace et les outils de construction et de restauration Sargo dans les sources publiques. Mettre à jour les README français et anglais avec les capacités et limites vérifiées. +- Corriger le contrôle de parité Android pour créer son dossier de rapports sur une copie neuve du dépôt. Vérifier que la configuration Wi-Fi native embarque un répertoire de connexions vide. +- Conserver les tickets, feuilles de route, documents internes, données personnelles et clés hors du dépôt. + +### Paquets et qualification + +- L’APK public reste Preview17 : les correctifs du 8 octobre nécessitent une prochaine compilation. Aucune nouvelle image native publiée ni flash dans cette étape. +- Sur Sargo, l’enregistrement cellulaire reste en recherche lors du dernier contrôle ; audio bidirectionnel, appairage Bluetooth et profils restent à qualifier. + + + +## 8 octobre 2026 — Caméra et parcours Contacts · en développement + +### Commun · Gaia / PC / Android / Tundra + +- Sérialiser les ouvertures de caméra, attendre la fermeture du capteur précédent et libérer les pistes et ressources vidéo même après un échec. Stabiliser l’identification avant/arrière sur PC et Android. +- Conserver les images binaires lors des échanges PC/Tundra ; encoder explicitement les photos et dates des contacts pour le pont JSON Android. +- Corriger l’actualisation de la fiche après modification d’un contact. Rétablir les activités Contacts → SMS, sélection d’un numéro et ouverture du pavé numérique, en préservant les demandes imbriquées et les points d’entrée de Gaia. +- Réouvrir Galerie en mode sélection pour ajouter une photo à un contact, même lorsque Galerie est déjà ouverte en consultation. Sélection, recadrage et enregistrement de la photo validés sur PC. +- Ajouter des tests de caméra, de stockage des contacts, de permissions, de transport binaire et de routage des activités. + +### PC + +- Vérifier le parcours original Appareil photo → capture JPEG → Galerie → ouverture de la photo → retour caméra. +- Vérifier dans Gaia : création et modification d’un contact, actualisation de la fiche, recherche sans accents et sans résultats, favoris, destinataire SMS, sélection d’un numéro, pavé numérique prérempli, annulation puis confirmation de suppression. Le test n’émet ni appel ni SMS et supprime son contact temporaire. + +### Tundra · Sargo + +- Vérifier en RAM sur Pixel 3a : autofocus, zoom, quatre alternances avant/arrière avec nouvelles trames, capture JPEG depuis Gaia, stockage, ouverture dans Galerie et arrêt du capteur à l’accueil. +- Vérifier les conversations déjà stockées, le clavier et les notifications. Recherche Wi-Fi : quatre réseaux détectés. Nouvelle émission/réception SMS et audio des appels encore en qualification. +- Ajouter les paquets BlueZ et les bibliothèques nécessaires, verrouillés par SHA256. Le contrôleur Bluetooth démarre, s’active et lance une recherche ; l’appairage depuis Gaia et les profils ne sont pas encore validés. +- Ajouter le magasin de certificats natif : HTTPS vérifié sans désactivation de TLS. Tester les archives altérées, collisions de fichiers et reconstructions répétées. + +- Corriger les fichiers Contacts embarqués dans le build de démarrage : la création, l’édition, le passage vers SMS, le sélecteur et le pavé numérique passent aussi avec les gestes tactiles sur Sargo. + +### Android · APK + +- Publier l’APK Preview17 existant, avec téléchargement et SHA256 vérifiés. Il précède les correctifs du 8 octobre. +- Corriger la transmission des destinataires et du texte de Gaia vers l’application SMS Android, y compris la réponse à une conversation et le numéro saisi sans validation préalable. Tests du pont réussis ; essai Android réel encore nécessaire. +- Réempaqueter les ressources communes corrigées pour la prochaine compilation ; aucun nouvel APK incluant ces correctifs n’est publié. + +**État :** builds natifs de qualification démarrés en RAM. Aucun flash, push de code ni publication de documentation privée dans cette étape. La qualification complète des fonctions de téléphone reste ouverte. + + + +## 6 octobre 2026 — Thème Marketplace intégré · en développement + +### Commun · Gaia / PC / Android / Tundra + +- Ouvrir le catalogue avec `mode=app`, conservé dans les liens, la recherche, les filtres, les fiches et le changement de langue. +- Préparer un thème web Vulpes dédié : Fira locale, navigation compacte, boutons tactiles, cartes lisibles, filtres repliables et captures défilantes. Retirer les en-têtes du site dans le mode intégré, tout en conservant l’interface du site classique. +- Conserver les confirmations et messages système côté Gaia. Le thème ne donne aucun privilège supplémentaire aux pages du catalogue. +- Vérifier 16 pages FR/EN, 2 pages en mode classique et les ressources du thème. Rendus mobiles de 320 et 360 pixels inspectés. Lanceurs PC et Sargo mis à jour ; les sources seront reprises au prochain empaquetage Android. + +Fichiers du site prêts localement, non déployés sur le serveur public. Aucun push ni nouvelle publication APK/ROM. + + + +## 6 octobre 2026 — Bandeaux Gaia · en développement + +### Commun · Gaia / PC / Android / Tundra + +- Faire disparaître le bandeau « Application installée » après son délai normal, même si la fin d’animation ne remonte pas du composant Gaia. Conserver son apparence et permettre sa fermeture au toucher. +- Annuler les anciens délais lorsqu’un nouveau message remplace le précédent pour éviter une disparition prématurée. +- Vérifier sur PC l’affichage, l’expiration, le remplacement et la fermeture au clic ; vérifier sur le Pixel le retrait effectif du bandeau après cinq secondes. Correctif déployé sur l’essai Sargo en RAM et disponible dans les sources communes pour le prochain APK. + +Aucun flash ni publication dans cette étape. + + + +## 6 octobre 2026 — Wi-Fi et panneaux Paramètres · en développement + +### Commun · Gaia / PC / Android / Tundra + +- Terminer l’ouverture d’un panneau Paramètres même si Gecko ne produit pas l’événement de fin d’animation. Conserver les transitions Gaia et éviter les doubles initialisations. +- Correctif dans les sources communes et le serveur PC ; inclus au prochain empaquetage Android. Aucun nouvel APK publié dans cette étape. + +### Tundra · Sargo + +- Corriger les événements de signal Wi-Fi : transmettre le réseau associé et ne pas annoncer de signal de connexion lorsque le téléphone est déconnecté. L’événement incomplet faisait échouer l’affichage des réseaux dans Gaia. +- Sur le Pixel, vérifier le pilote, le service natif, le passage par Gecko et l’affichage du panneau corrigé : quatre points d’accès détectés, regroupés en deux réseaux, sans erreur de recherche. +- Connexion Wi-Fi confirmée par l’utilisateur ; requête HTTPS réelle réussie depuis Gecko vers example.org (HTTP 200, vérification des certificats active). Correctifs appliqués à l’essai en RAM ; aucun flash ni push. + + + +## 6 octobre 2026 — Marketplace · en développement + +### PC / Tundra · Sargo + +- Maintenir le catalogue sous la barre de statut et dans les limites de son application. Masquer sa vue web à l’accueil et dans le multitâche ; la retirer à la fermeture de l’application. +- Éviter qu’une navigation annulée ou l’ancienne page d’erreur masque une nouvelle tentative. +- Afficher un message local et proposer une nouvelle tentative lorsque le catalogue est inaccessible, au lieu de laisser sa page d’erreur recouvrir Gaia. +- Sur PC : navigation, erreur réseau, nouvelle tentative, retour à l’accueil et fermeture depuis le multitâche vérifiés. Sept contrôles directs passent sur Sargo en RAM, sans fenêtre de débogage, avec une page locale et une précondition de déverrouillage forcée ; le catalogue distant nécessite une connexion Internet sur le téléphone. + +### Android · APK + +- Reconstruire l’APK local Preview17 avec le message d’erreur Marketplace commun. Signature, alignement et parité des ressources vérifiés ; nouvelle régression sur appareil Android non effectuée dans cette étape. + +Aucune publication dans cette étape. + + + +## 2.7-preview.17 — APK construit · 5 octobre 2026 + +### Commun · Gaia / PC + +- Remettre Marketplace sur l’accueil avec son icône originale et le catalogue Vulpes. Ouvrir sa version française pour les langues fr, anglaise sinon. +- Adapter les téléchargements du catalogue en installations confirmées : vérifier les archives ZIP, ajouter l’application à l’accueil et permettre son lancement et sa désinstallation. +- Conserver les applications installées séparément des sources Gaia, sur des origines distinctes et sans privilèges système hérités des anciens manifestes. +- Valider sur PC le parcours réel avec 2048 : catalogue, confirmation, installation, lancement, persistance et désinstallation. Contrôler les archives corrompues, chemins invalides, limites, annulation et confirmations à usage unique. + +### Android · APK + +- Construire et signer Preview17 avec le même Marketplace et le même service d’installation. Utiliser la vue GeckoView existante pour le catalogue et le stockage privé Android pour les paquets. +- Vérifier sur émulateur Android 16/API36 le clic Installer, la confirmation, l’apparition sur l’accueil et le lancement de 2048 sans accès au pont système Vulpes. Vérifier aussi la conservation après arrêt forcé et relance, puis la désinstallation. + +### Tundra · Sargo + +- Préparer une image de boot locale incluant Marketplace, l’installateur et le serveur des applications ajoutées. Démarrage en RAM effectué sur le Pixel le 6 octobre : accueil et ressources Marketplace vérifiés, sans flash. Neuf contrôles système sur dix passent ; le contrôle global Gaia relève une variation du viewport au déverrouillage. Installation de paquets sur Sargo encore à qualifier. + +**Limites :** compatibilité initiale avec les applications web empaquetées. Les anciennes API privilégiées, applications hébergées et services disparus ne sont pas restaurés automatiquement. Rien publié sur Forgejo ni sur le site. + + + +## 2.7-preview.16 — APK construit · 5 octobre 2026 + +### Commun · Gaia / PC + +- Rétablir les sélecteurs Gaia de fond d’écran et de sons, leur retour vers Paramètres et leur annulation par Accueil. Partager le transport des images et sons enregistrés entre PC et Android. +- Propager les changements de langue à l’accueil, y compris sans permission de lecture générale des réglages. Vérifier les libellés FR/EN et la persistance. +- Rétablir l’arrêt du balayage horizontal sur une page et masquer les applications de développement sur l’accueil, sans les supprimer des sources. +- Utiliser le logo Vulpes fourni dans le splash et adapter les composants originaux de la liste des sons au moteur actuel. +- Vérifier sur PC deux changements consécutifs de fond, le choix d’un son et les panneaux Gaia restaurés. + +### Android · APK + +- APK local Preview16 construit et signé ; contenu commun et version vérifiés. Aucune publication dans cette étape. +- Valider Vulpes comme lanceur HOME, afficher le verrouillage Gaia après veille et rétablir le retour Android depuis la caméra ouverte au verrouillage. +- Masquer les barres système Android en mode immersif, en conservant leur accès temporaire par balayage. +- Tests sur émulateur Android 16/API36 : verrouillage, caméra/retour, FR/EN, accueil sans applications de test, fond d’écran et son. Fond conservé après arrêt forcé et relance, sans changement répété pendant 15 secondes. Autres appareils Android non qualifiés dans cette étape. + +### Tundra · Sargo + +- Inclure les mêmes correctifs et la politique des applications dans une nouvelle image de boot de test. Le boot précédent réappliquait une copie ancienne des fichiers et annulait les modifications à chaud. +- Démarrage en RAM sur le Pixel réussi : six contrôles de langues/accueil/sélecteurs et dix contrôles système passent. Sélecteurs image/son vérifiés sans écraser les choix personnels. Le boot installé reste Preview15 ; aucune partition flashée dans cette étape. + +**Limites :** l’installation tierce FFAA reste ouverte. Le site dépend d’anciennes API mozApps et d’un script nomodule ; un installateur isolant les applications et leurs permissions reste à construire. Le choix d’un son Gaia ne modifie pas la sonnerie globale Android ; l’audio d’appel réel n’a pas été qualifié ici. + + + +## En développement — 4 octobre 2026 — Installation autonome Sargo + +### Tundra · Sargo + +- Construction d’un volume ext4 neuf directement destiné à userdata : système, données et composants matériels vendor/modem embarqués. Le démarrage ne cherche plus de fichiers dans une installation Droidian ; aucune session Android ni connexion ADB n’est nécessaire à l’installation. +- Installateur Fastboot en Python : contrôle du modèle Sargo, du déverrouillage, des tailles et des empreintes, puis remplacement de userdata, dtbo_a, vbmeta_a et boot_a. Le bootloader reste déverrouillé ; les calibrations propres au téléphone sont conservées. +- Sauvegarde complète de userdata hors ligne, comparaison intégrale avec le téléphone et vérification de l’archive de restauration avant le flash. Les sauvegardes restent privées ; une restauration réelle n’a pas été effectuée dans cette étape. +- Deux démarrages depuis le stockage interne réussis : onze contrôles Gaia et dix contrôles natifs par démarrage, image de boot relue et vérifiée, réglages conservés. Le slot A est marqué réussi uniquement après validation du démarrage Gaia courant. +- Variante générique sans serveur SSH ni clés de maintenance : flash de boot_a et démarrage réel réussis. La variante de développement est ensuite remise sur le Pixel pour poursuivre les travaux. Les images du paquet générique conservent leur état initial vierge. +- Contrôles finaux sur le démarrage autonome de développement : Photo (dont bascules de capteur), clavier Gaia et notifications réussis. +- Paquet Fastboot local assemblé avec empreintes et rapport de qualification. 44 tests ciblés passent. Il n’est pas encore publié : la provenance et la redistribution de certains composants matériels restent à finaliser. +- Limites : qualification sur un Pixel 3a, pas sur toutes les combinaisons de firmware ; volume de 8 Gio comprenant 1 Gio de données modifiables, sans extension automatique. Les fonctionnalités matérielles restent expérimentales. + +### PC / Android · APK + +- Aucun changement d’interface ni nouveau binaire dans cette étape. Gaia reste partagé entre les trois plateformes. + + + +## En développement — 3 octobre 2026 — Installation Sargo par appareil + +### Tundra · Sargo + +- Collecte des six partitions de démarrage en lecture seule et du stockage existant, liés à l’identité du téléphone et au slot A. Les sauvegardes restent privées. +- Dossier appareil portable : construction RAM, contrôle du boot, installation et export du secours peuvent l’utiliser sans l’inventaire partagé du Pixel de développement. Un build lié à un dossier appareil refuse le repli implicite vers cet inventaire. +- Transfert SSH contrôlé : vérification de l’identité et du stockage, contrôle des empreintes et création des fichiers sans remplacer les données existantes. Aucun formatage, flash ou redémarrage dans l’outil de transfert. +- Construction et transfert testés sur le Pixel. Premier boot RAM : onze contrôles Gaia et dix contrôles natifs réussis ; clavier et notifications réussis. Le contrôle caméra a rencontré un délai d’autofocus dépassé, conservé dans le rapport. +- Second boot de la même image : réglage Gaia conservé, onze contrôles Gaia et dix contrôles natifs réussis ; caméra, clavier et notifications réussis. Le timeout autofocus du premier essai reste documenté, pas déclaré corrigé. +- Retour au build installé vérifié : démarrage Gaia réussi et partition de boot inchangée. Un nouveau transfert refuse bien de remplacer les données déjà utilisées. +- Provenance du HAL précisée : paquet et recette de téléchargement identifiés, recettes UBports historiques récupérées. La correspondance exacte avec l’ancien artefact Android et son manifeste de sources reste à établir. +- Prise en charge toujours limitée au noyau qualifié et au stockage ext4/LVM compatible. Ce travail ne fournit pas encore une installation directe depuis Android d’origine. + +### PC / Android · APK + +- Aucun changement d’interface ni nouvelle compilation dans cette étape. + + + +## En développement — 3 octobre 2026 — Préparation et secours Sargo + +### Tundra · Sargo + +- Ajout d’un outil de préparation locale : contrôle du manifeste et de l’archive, décompression bornée, vérification ext4 et création d’un espace de données vierge propre à l’installation. Les dossiers existants ne sont pas écrasés. +- Ajout d’un kit de restauration privé exportable hors du dépôt. Il conserve la sauvegarde originale de boot_a et un outil autonome qui vérifie le modèle, le numéro de série, le slot et les empreintes avant toute restauration explicite. Il ne constitue pas une sauvegarde des données utilisateur. +- La restauration existante ne dépend plus de la présence de l’image de diagnostic. Les contrôles de la sauvegarde sont conservés. +- Construction système : référence matérielle ARM64 désormais explicite, avec refus d’une référence limitée à l’affichage ou dépourvue des services requis. +- Préparation de l’archive système réelle et vérification locale du kit de secours effectuées. Aucun nouveau flash ni nouvelle ROM publiée ; l’installation automatique sur un autre Sargo reste à finaliser. + +### Commun · PC / Android / Tundra + +- Le journal JSON partagé est conservé dans les futures publications de sources ; les documents de travail Markdown restent exclus. + +### PC / Android · APK + +- Aucune modification fonctionnelle ni nouvelle compilation dans cette étape. + + + +## En développement — 3 octobre 2026 — Sargo : récupération modem et installation vierge + +### Tundra · Sargo + +- Reconstruction de l’image système avec le correctif du cache des polices et la création du répertoire privé d’oFono au premier démarrage. +- Récupération limitée au sous-système modem : test contrôlé avec passage hors ligne, remise en service et réenregistrement réseau en environ 22 secondes, sans redémarrage du téléphone. Cela ne corrige pas la cause du watchdog IMS du firmware. +- Démarrage Gaia, rendu matériel, polices Fira, deux capteurs photo, focus/zoom, clavier et notifications visuelles contrôlés sur le profil neuf. Scan Wi-Fi encore fonctionnel après le redémarrage du modem. +- Deux démarrages réussis avec conservation du même réglage Gaia sur un espace de données séparé. Caméra, clavier et notifications revérifiés au second démarrage. La partition de boot installée et les données existantes sont conservées. +- Aucune nouvelle image native publiée : le paquet public d’installation/restauration reste à terminer. + +### PC / Android · APK + +- Aucun changement de l’interface commune ni nouvelle compilation APK dans cette étape. + + + +## 1er octobre 2026 — Sargo : préparation de la distribution — en développement + +### Tundra · Sargo + +- Image système Preview 15 reconstruite avec profils vierges, exclusion des documents de travail et conservation des licences ; inventaire des 493 paquets embarqués. +- Vérification ext4 et contrôle du contenu effectués. Ces contrôles ne constituent pas une validation de démarrage. +- Inventaire du stockage en lecture seule et génération de la table de montage depuis cet inventaire, au lieu de reprendre systématiquement les dimensions du Pixel de développement. Prise en charge limitée au volume ext4 à une seule étendue sur userdata Sargo ; aucun formatage ajouté. +- Image nettoyée transférée et vérifiée sur le Pixel chargé. Essais temporaires en RAM, sans écriture de boot_a ; retour automatique vers le build v59 et redémarrage de Gaia contrôlés. +- SIM reconnue, enregistrement LTE et scan Wi-Fi contrôlés sur cette image ; ces contrôles ne valident pas les appels, SMS ou données mobiles de bout en bout. +- Correction des droits du cache des polices au premier démarrage : fc-cache utilise le compte de session. Démarrage Gaia, rendu matériel, Fira, deux capteurs photo, clavier et notifications visuelles contrôlés en RAM. La qualification du fichier reconstruit reste incomplète. +- Redémarrage intermittent identifié dans le firmware IMS du modem ; politique de récupération ciblée appliquée et second démarrage avec LTE, caméra, clavier et notifications réussi. La récupération après un nouveau crash IMS reste non testée. L’image publique reste non flashable et non publiée. + +### PC / APK + +- Aucun changement fonctionnel ni nouvelle compilation APK dans cette étape. + + + +## 2.7-preview.15 — 30 septembre 2026 — publiée + +[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15) + +### Commun · Gaia / PC / Android / Tundra + +- Première publication des sources communes, avec README français et anglais, licences et scripts de préparation. Documentation interne, roadmap, profils, clés et images privées exclus du dépôt public. +- Intégration des derniers correctifs SMS (dates des bulles, états d’envoi/échec) et du cycle de vie caméra. Les deux entrées du 30 septembre détaillent les correctifs matériels. + +### PC + +- Copie de publication testée avec moteur Gecko 156.0.1 et profil neuf : démarrage Gaia, navigation, réglages, gestes, historique et isolation des pages. 42 tests de services passants ; persistance des services après redémarrage vérifiée. +- Stabilisation du test de navigation lors du remplacement normal d’un document et allongement de l’attente de démarrage à froid. Fermeture propre de l’hôte même si Gaia dépasse le délai de préparation. + +### Android · APK + +- Preview 15 signée avec la clé des précédentes Preview, GeckoView 156.0 ; parité des ressources communes vérifiée. Mise à jour installée sur émulateur Android 16. +- Photo et Galerie : capture, affichage et reprise du capteur validés. Dix passages en arrière-plan et récupération après arrêt du processus d’affichage validés. Cette qualification ne couvre pas tous les téléphones Android. + +### Tundra · Sargo + +- Sources du runtime incluses ; l’image privée v59 reste celle qualifiée sur le téléphone de développement. Aucune ROM générique flashable publiée : démarrage encore lié au stockage inventorié, aux images préinstallées et aux accès de maintenance privés. +- Limites conservées : aperçu caméra proche de 16 i/s, flash/vidéo, rotation et veille profonde incomplets ; audio d’appel à qualifier et grand dialogue après envoi SMS à identifier. + + + +## En développement — retours SMS et caméra · 30 septembre 2026 + +### Commun · Gaia / PC / Tundra + +- SMS : convertir les dates en nombres avant de construire les bulles. Corrige les conversations vides dues à une erreur de formatage de date. Parcours du compositeur et affichage des messages envoyés/reçus vérifiés sur PC avec un modem simulé. +- Photo : fermeture du flux rendue idempotente; détacher les vidéos, attendre la libération du capteur et libérer le contexte WebGL avant de changer de caméra. Identifier chaque session pour qu’un ancien flux ne puisse pas arrêter son remplaçant. Allers-retours arrière/avant vérifiés avec de nouvelles trames à chaque bascule; retour Accueil et arrêt du capteur validés lors de deux démarrages RAM. + +### Tundra / Sargo + +- Vibreur : utiliser le contrôle brightness du pilote DRV2624; la commande legacy activate ne coupait pas physiquement le moteur. Coupure confirmée par l'utilisateur; impulsion, motif 200/200/200 ms et arrêt du service vérifiés, avec retour à zéro. Ajouter une coupure de secours au démarrage et à la sortie du service. +- L'utilisateur confirme focus, zoom et transport SMS dans les deux sens sur v54. Cinq conversations existantes affichées sur le Pixel, avec leurs bulles et sans erreur de date. +- v59 installé sur boot_a après deux démarrages RAM réussis; démarrage installé et relecture vérifiés, slot A marqué réussi. Sources et runtime PC synchronisés. Le grand dialogue après un envoi réel reste à identifier; la fluidité caméra reste un chantier distinct. + +### Android · APK + +- Sources Gaia communes corrigées; aucun nouvel APK construit ou publié. + + + +## En développement — SMS et Photo · 30 septembre 2026 + +### PC / Tundra + +- SMS : conserver le message et sa conversation après un refus du modem; transmettre les événements Gaia « envoi », « échec » et « envoyé ». +- SMS : distinguer mise en file oFono et confirmation d'envoi, avec suivi persistant des états terminaux. La confirmation modem n'est pas un accusé de réception du destinataire. +- PC : tests de l'échec d'envoi visible dans la liste des conversations; Photo et Galerie testées avec capture tactile, ouverture, fermeture et reprise de la caméra. + +### Tundra / Sargo + +- Photo : raccorder les zones de focus tactile, le zoom numérique et la capture JPEG du capteur à l'interface Gaia originale. Aperçu conservé en 640 × 480; JPEG arrière 4032 × 3024 (12,19 Mpx) et avant 3264 × 2448 (7,99 Mpx) mesurés directement. Focus fixe à l'avant. +- Protocole caméra : séparer les traces du HAL des données binaires, borner les réponses et relancer l'aperçu après une capture. Une erreur de focus ne doit plus fermer la session. +- SMS : refuser explicitement l'envoi si le modem n'est pas enregistré; conserver les états sent/failed même après disparition de l'objet oFono. La réception a produit des notifications opérateur sur v49 après retour du réseau LTE. +- Qualification : deux démarrages v54 en RAM puis démarrage depuis boot_a validés; image installée sur le Pixel et slot marqué réussi après relecture. Persistance, interface Gaia et tests caméra passants. L'enregistrement réseau reste instable : essais SMS arrêtés avant émission, aucune livraison de bout en bout confirmée. + +### Android · APK + +- Aucun nouvel APK construit ou publié dans cette passe. La Preview 14 disponible reste inchangée. + + + +## 2.7-preview.14 — APK publié · 30 septembre 2026 + +[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.14) + +L’APK Preview 14 est publié. La version PC est à jour et le build privé Sargo v48 est installé et testé ; aucune image téléphone n’est publiée avec cette release. + +### Commun · PC / APK / Gaia + +- Informations : suppression du retrait supplémentaire des lignes de version avant « Dernière mise à jour » ; alignement contrôlé sur PC et Android. +- Polices : normalisation des noms de fontes dans les feuilles des composants Shadow DOM, y compris les styles ajoutés après leur création. Les polices d’icônes restent distinctes. +- Calendrier : ouverture du mois et absence d’erreur JavaScript vérifiées sur PC. + +### PC / Tundra + +- Clavier : calculer le mode portrait depuis l’écran, et non depuis la petite fenêtre du clavier. Touches de 60 px CSS environ sur Pixel ; rotation et retour au portrait testés sur PC à densité 3. +- SMS : replacer le curseur dans le champ actif lorsque Gecko conserve la sélection du destinataire ; conserver les sélections valides pour l’édition. +- Notifications Gaia : respecter les indicateurs silencieux, sans son et sans vibration, y compris à la restauration. + +- Clavier Gaia : pont de saisie natif, pavé numérique pour les destinataires SMS, événements de sélection corrigés et protection contre les réponses d’un champ devenu inactif. Saisie destinataire et message testée sur PC et Pixel ; l’envoi opérateur reste à qualifier. +- Verrouillage : accepter l’absence de fenêtre clavier lors d’un redimensionnement. +- Notifications natives : stockage, propriété par application et ouverture du point d’entrée Téléphone déclaré par Gaia. Présentation et suppression dans le volet et sur le verrouillage validées sur Sargo ; écoute du son à qualifier. + +### Tundra / Sargo — build v48 installé + +- Vibrations : pont vers le moteur Sargo, motifs bornés à dix secondes, séquencement sans bloquer les boutons physiques et annulation. Activation et arrêt mesurés sur le pilote. + +- Wi-Fi : profils privés conservés avec reconnexion automatique et commande d’oubli ; tests du stockage et de la restauration en cas d’échec. Connexion et reconnexion après redémarrage RAM vérifiées sur Sargo ; fichiers de profil privés (0600). +- SMS entrants : stockage persistant avant acquittement, dédoublonnage des reprises et services créant leur dossier d’état au démarrage. Défaut de montage du dossier corrigé après reproduction sur Sargo. +- Audio : commandes écouteur/haut-parleur, micro et volume d’appel ajoutées au pont ; bascule écouteur/haut-parleur, sourdine et volume vérifiés sans appel. Conversation dans les deux sens encore à vérifier. +- Images de diagnostic avec débogueur local interdites au flash par l’outil d’installation. v48/preview.14 écrite sur boot_a après deux démarrages RAM et contrôle de persistance ; démarrage installé validé, image relue conforme et slot A marqué réussi. v43 à v48 n’activent pas de débogueur. + +### Android · APK + +- APK preview.14 construit, signé et publié sur Forgejo. Démarrage, informations de version, alignement et sélection des fontes des composants contrôlés sur Android 16 émulé. Le clavier Android reste celui du système. + + + +## 2.7-preview.13 — parcours dans les applications · 27 septembre 2026 + +### Commun · Gaia / PC + +- Calendrier : une alarme planifiée avec succès ne provoque plus un rejet lors de la sauvegarde de l’événement; les erreurs de planification sont transmises au demandeur. +- Rappels : ignorer ceux d’événements supprimés; ne plus marquer une notification refusée comme livrée. La prise en charge des notifications natives Tundra reste à terminer. +- Notifications : accepter un texte brut vide sans chercher une traduction « undefined ». +- Galerie : vignettes de taille positive même au démarrage masqué; annulation des mises à jour d’aperçu après fermeture de l’éditeur. +- SMS : appliquer les filtres correspondant, lu/non lu, période et livraison; tests de lecture et suppression des conversations. +- Revue approfondie : lecture, déplacement et suppression d’une vidéo WebM; édition noir et blanc et copie d’une photo; création et suppression d’un événement. Interfaces Gaia originales conservées. + +- Qualification depuis une copie isolée : démarrage, tests bureau, persistance et empaquetage Android réussis sans réécriture des sources. + +### Android · APK + +- APK preview.13 construit et signé; Photo/Galerie, Calendrier, dix retours d’arrière-plan et récupération du rendu validés sur Android 16 émulé. + +### Tundra · Sargo + +- Build v40/preview.13 installé après deux démarrages RAM : Gaia, caméra, Wi-Fi, luminosité et commandes contrôlés; relecture de boot_a conforme et slot validé. Limites restantes dans `docs/APPLICATION-REVIEW.md`. + + + +## 2.7-preview.12 — revue des applications · 27 septembre 2026 + +### Commun · Gaia / PC + +- Icônes internes : variantes raster Gaia originales de 1× à 2,25× rétablies dans + 1 047 références CSS. Les icônes vectorielles et les polices restent originales. +- Photo : bouton de changement de capteur réactivé après découverte des caméras; + flash indisponible visible et grisé; le pincement ne zoome plus le document Gaia. +- Accueil : gestes natifs réservés au bouton pendant l’appui long; menu contextuel + désactivé sur ce bouton. Le gestionnaire des tâches Gaia reste utilisé. +- Transmission des gestes entre System et les applications restaurée avec des + événements DOM non privilégiés, sans autoriser de faux gestes sur les sites Web. +- Navigateur : intégration de la page `browser-welcome` fournie, locale, bilingue, + sans suivi; recherches et liens transmis au véritable navigateur. +- Page d’accueil : suit automatiquement la langue Gaia, `fr`/`fr-*` en français, + les autres en anglais; changement appliqué sans redémarrage. +- SMS : identifiants compatibles avec le registre numérique Gaia, migration des + anciens messages, dates de l’API et événement d’envoi corrigés; écritures sérialisées. +- Calendrier : libellé d’accessibilité du jour courant retrouvé dans 37 langues. +- Revue de 14 applications; 26 contrôles fonctionnels Galerie/Musique/Horloge/UI, + tests Contacts et SMS, et navigation Web isolée. Voir `docs/APPLICATION-REVIEW.md`. + +### Android · APK + +- APK `2.7-preview.12` : même interface, correctifs et page locale; reconstruction + et parité contrôlées. L’accès caméra reste celui d’Android via getUserMedia. +- Aucun portage automatique des fonctions matérielles Tundra vers Android : chaque + plateforme conserve son adaptateur et annonce ses capacités réelles. + +### Tundra · Sargo + +- Aperçu Photo : conversion NV21 par shader, transport binaire au lieu de base64; + environ 23 images/s mesurées en RAM contre environ 8 dans l’essai intermédiaire. +- Fermeture du worker attendue avant de rouvrir un autre capteur. +- Luminosité manuelle : retrait des animations successives, environ 200 ms pour + atteindre la valeur demandée lors du test matériel; transitions veille/réveil conservées. +- Wi-Fi : état radio réel et activation/désactivation raccordés aux réglages Gaia; + réseau mixte WPA2/WPA3 compatible avec le formulaire WPA-PSK. +- Scan par Gaia, connexion et navigation HTTPS par wlan0 validés en RAM. Les + profils de connexion restent temporaires; leur persistance est encore à réaliser. +- Image v38 installée : deux démarrages RAM, démarrage installé, relecture de + `boot_a`, préservation des autres partitions et validation du slot A réussis. +- Flash photo, vidéo, capteur de luminosité/rotation, Bluetooth, comptage de données + et fonctions avancées d’appel restent incomplets. Un écran qui s’ouvre ne vaut + pas validation de tout son matériel. + + + +## En développement — retours Sargo et interface · 27 septembre 2026 + +### Commun · Gaia / PC + +- Animation d’arrêt : trois nuances bleues remplacent les ronds orange; dimensions, + séquence et durées de l’animation Gaia sont conservées. + +- Téléphone : empêcher le défilement du document lors du changement d’onglet; + les en-têtes Journal et Contacts restent en place. Les icônes du journal utilisent + les images Gaia originales en plusieurs densités, jusqu’à 2,25×. +- Menus déroulants : retrait du décalage prévu pour les anciens moteurs, qui coupait + les jambages de Fira. Vidéo : texte de présentation contenu dans la fenêtre. +- Luminosité : le curseur enregistre un nombre et répond aux événements input/change; + les anciennes valeurs textuelles sont converties avant la transition Gaia. La file + matérielle regroupe les pas obsolètes de luminosité sans réordonner veille/réveil. +- Accueil : le fond d’écran continue sous le dégradé transparent. L’espace de navigation + reste réservé dans les applications; en paysage, le bouton se place à droite. +- Navigateur : page locale éditable dans `overrides/search.gaiamobile.org/home/index.html`. + Les branchements de Gaia et le mode privé restent en place. +- Wi-Fi : un échec affiche une explication et le bouton de reprise au lieu de relancer + indéfiniment le scanner. La recherche et l’affichage dans le panneau Gaia ont + maintenant été vérifiés sur Sargo en RAM. +- Suivi consolidé : `docs/CHANTIER.md`, avec anciens et nouveaux retours, preuves et limites. + +### Android · APK + +- APK `2.7-preview.11` construit et signé avec la même base Gaia; parité vérifiée. + Barre Accueil à droite en paysage et correction du fond en mode Accueil. + Rotation et absence de recouvrement vérifiées sur émulateur Android 16. + +### Tundra · Sargo + +- Caméra : aperçu matériel dans Photo Gaia, JPEG et bascule arrière/avant validés + en RAM sur Sargo; les capteurs se ferment au retour à l’accueil. Capture initiale + limitée à 640 × 480 (480 × 640 en portrait); vidéo et flash non disponibles. + L’interface reste commune au PC et à l’APK, qui conservent getUserMedia. + +- v33 (`2.7-preview.11`) installé après deux démarrages RAM réussis. Démarrage installé, relecture de + `boot_a`, conservation des données et finalisation du slot A vérifiés. +- Luminosité mesurée depuis Gaia : 35 % → 89/255, 80 % → 204/255. Résultats Wi-Fi + visibles dans le panneau original. Animation d’arrêt bleue incluse. +- Routage audio : prise en charge du format de PulseAudio 14; suppression du double + chargement des modules audio. Passage au profil téléphonique et à l’écouteur, puis + restauration du profil média, vérifiés sur Sargo. Conversation réelle encore à valider. +- Restent ouverts : conversation audible dans les deux sens, capture pleine résolution/vidéo et + raccordement des capteurs pour la rotation automatique. Voir la liste de suivi. + + + +## En développement — stabilité Wi-Fi native · 27 septembre 2026 + +### Commun · Gaia / PC + +- Correction de la boucle Wi-Fi sans délai : recherche toutes les 20 secondes, + conformément à l’intervalle prévu par Gaia. Regroupement des scans simultanés. +- Version de développement commune : `2.7-preview.9`, Gaia `2.7.0`; versions + réelles de Gecko inchangées. Tests de rafales, erreurs et temporisation ajoutés. + +### Android · APK + +- Nouveau paquet `2.7-preview.9` pour conserver la même base Gaia. Le Wi-Fi Android + reste géré par Android; ce correctif ne prétend pas ajouter un pilote à l’APK. + +### Tundra · Sargo + +- Cause du blocage v21/v22 identifiée : multiplication des clients Python de + recherche Wi-Fi, épuisement mémoire et arrêt de Gecko par le lowmemorykiller. + Le SIGSEGV de phoc survient ensuite pendant sa fermeture. +- Un seul scan matériel en cours, cache court des résultats et des erreurs, + invalidation après connexion/déconnexion. Aucun affaiblissement de l’isolation + des applications, ni désactivation du mécanisme de protection mémoire. +- Candidat v25 qualifié sur deux démarrages RAM, puis installé et vérifié sur le + Pixel : 11 contrôles Gaia et 10 contrôles système passent, sans destruction de + processus par manque de mémoire. Verrouillage Gaia, Fira, batterie, date et + panneau Wi-Fi contrôlés; veille/réveil et volume testés par événements injectés. +- Relecture de `boot_a` conforme, autres partitions de démarrage inchangées, + données conservées et slot A marqué comme démarré avec succès. La caméra native + et la validation complète des appels restent à terminer. + + + +## En développement — restauration Gaia · 27 septembre 2026 + +### Commun · Gaia / PC + +- Restauration des panneaux Gaia et suppression des fenêtres/boutons provisoires : + Paramètres, Suivi conso, FM et import de contacts/médias. Les fonctions sans API + restent visibles avec commandes désactivées, sans valeurs matérielles inventées. +- Correction de la portée des anciens styles : les sélecteurs `div` ne sont plus + transformés en racines de composants. Retour des icônes du verrouillage, + conservation de Fira et glyphes Gaia vectoriels pour les onglets Téléphone. +- Accueil dispose de 44 pixels CSS réservés et ne recouvre plus Décrocher/Raccrocher. + La date utilise le composant historique; la batterie est actualisée dans Paramètres + et ses libellés n’affichent plus `{{ level }}`. +- Écran de verrouillage Gaia réactivé; splash provisoire avec le logo fourni, + dimensions stables et animation douce. Nom localisé : Navigateur / Browser. + Remerciements dans Informations légales, avis de licences conservés. +- Tests PC : 26 contrôles applicatifs passent (Galerie, Musique, Horloge, tâches), + panneaux, imports, batterie, date et navigation; appels simulés, + journal, veille/réveil, menu Power et volume. Voir `docs/GAIA-RESTORATION.md`. + +### Android · APK + +- APK `2.7-preview.8` construit et installé sur l’émulateur Android 16, avec la même base Gaia; batterie lue depuis Android, + Paramètres Android en bas de liste et mode Accueil Android optionnel. Le catalogue + des applications Android dans Gaia reste à raccorder. +- Validation Android : dix reprises après changement d’application et récupération + après destruction du renderer. Parité du contenu partagé vérifiée; pas de test + physique Android dans cette passe. +- Les imports utilisent les composants Gaia; le bouton SMS ouvre la confirmation + Android. Le verrouillage interne ne remplace pas la sécurité Android. + +### Tundra · Sargo + +- Candidats v21/v22 construits avec tous les correctifs `overrides/` dans le paquet de boot; + raccordement scan/association Wi-Fi au panneau Gaia via NetworkManager. +- Essais RAM bloqués : service graphique arrêté avec le code 139 avant qualification. + Aucun nouveau flash. Retour sur v20 installé validé (11 contrôles Gaia, 9 système, + données conservées); verrouillage du profil natif remis à son état précédent. + Wi-Fi du candidat, caméra native, API SIM/Bluetooth et suspension CPU non qualifiés. + + + +## En développement — 27 septembre 2026 + +### Commun · Gaia / PC + +- Remplacement de l’écran d’appel provisoire par **Callscreen**, l’interface + originale de Gaia : appel entrant/sortant, réponse, raccrochage et fermeture. + L’adaptateur fournit les événements `mozTelephony` aux modules historiques. + Les contrôles audio/conférence non encore raccordés restent grisés. +- Journal d’appels persistant côté hôte, puis import dans le vrai journal Gaia, + avec acquittement et détection des doublons après interruption. +- Batterie : pont vers l’icône historique ; une batterie inconnue n’est pas + présentée comme vide. Correction de l’index de contacts Facebook absent ou + mal formé, qui bloquait aussi la recherche de numéro et l’historique local. +- Polices : comparaison des glyphes réellement rendus. L’accueil utilisait + déjà Fira Sans des deux côtés, mais certains textes système utilisaient Noto + sur PC. Les règles de texte principales sélectionnent désormais la Fira de + Gaia ; les polices d’icônes sont conservées. +- Test Gecko avec modem simulé : appels entrants/sortants, boutons Répondre et + Raccrocher, journal, veille/réveil, menu Power, volume et batterie. + Aucun nouvel APK compilé dans cette étape. + +- Étape v13 (remplacée par Callscreen ci-dessus) : écran d’appel géré par l’application système : numérotation, sonnerie, + communication et bouton Raccrocher. Les contrôles restent présents si le + service perd momentanément l’état de l’appel. +- Tests sur le vrai Gecko bureau avec modem simulé, y compris échec puis nouvel + essai de raccrochage. Sources PC synchronisées ; aucun APK reconstruit. + +### Tundra · Sargo + +- Build privé v20 installé et relu dans `boot_a`, après deux démarrages RAM + et un démarrage depuis le stockage ; réglages conservés, slot marqué réussi. +- Lecture des seules touches Power/Volume du Sargo et + transmission à `HardwareButtons`. Appui court : écran éteint/rallumé ; appui + long : menu Gaia. Le broker natif commande rétroéclairage, volume, arrêt et + redémarrage avec une liste d’opérations fixe. +- Sérialisation des changements de luminosité et d’alimentation : deux + requêtes simultanées ne peuvent plus laisser le rétroéclairage éteint au réveil. +- Volume natif : accès en lecture au socket PulseAudio de la session, sans + lequel l’indicateur Gaia changeait mais la sortie audio restait inchangée. +- La veille écran ne suspend pas encore le noyau. Réception réelle des appels, + audio de conversation et commandes audio avancées restent à qualifier. + +- Étape précédente : build privé v13 installé dans `boot_a` après deux démarrages RAM ; démarrage + depuis le stockage, relecture et indicateur de réussite A/B vérifiés. +- Transmission des appels et de leurs états depuis oFono ; commandes Répondre et + Raccrocher limitées aux chemins d’appel du modem. +- Correction des réponses D-Bus sans contenu : un raccrochage réussi ne provoque + plus une fausse erreur de réponse invalide. +- Un appel réel court atteint la sonnerie puis est raccroché automatiquement. + Cela ne qualifie pas encore l’audio en conversation ni les appels entrants. +- Caméra : diagnostic corrigé, sans annoncer une initialisation à partir d’un + simple délai d’attente. Deux capteurs sont détectés via libhybris, mais le test + de flux reçoit zéro image. La caméra Gaia native reste indisponible ; voir la + [note technique](tundra/docs/CAMERA-SARGO.md). + + + +## En développement — 26 septembre 2026 + +### Commun · Gaia + +- Polices : Fira Sans OpenType du simulateur historique, avec les mêmes fichiers + pour les pages Gaia et le système Tundra. Correction du repli `sans-serif` natif + et vérification Fontconfig ajoutée ; les polices d’icônes restent distinctes. +- Paramètres : retrait des boutons de diagnostic « Paramètres Tundra » et + « Tester une notification ». Le résumé des versions et les informations + techniques restent disponibles. +- Panneau matériel : transmission des capacités de la plateforme au panneau. + Une variable absente provoquait une alerte Gecko au lieu d’afficher la batterie. + +### PC + +- Adaptateur local synchronisé avec les sources communes. Gecko reste en 156.0.1. + +### Android + +- Correctif du panneau présent dans les sources communes ; aucun nouvel APK construit. + +### Tundra / Sargo + +- Validation explicite du succès A/B après installation : sauvegarde et contrôle + des métadonnées GPT, outil Qualcomm vérifié, confirmation Fastboot. +- Erreurs du modem distinguées des erreurs de transport ; boîte d’erreur intégrée + à Gaia. La caméra native indisponible ne suggère plus une autorisation inutile. +- Diagnostic des polices réellement utilisées par Gecko et du réseau radio, sans + appel ni SMS automatique. Fira Sans confirmée sur l’accueil et la barre d’état. +- Installation réelle du v9 polices dans `boot_a` sur le Sargo inventorié. + Contenu écrit relu et conforme ; cinq autres partitions de boot sauvegardées intactes. +- Deux démarrages depuis le stockage : 10 contrôles Gaia et 9 contrôles système + réussis par boot, réglages conservés. Le PC n’est plus nécessaire pour démarrer. + Fichiers de secours Droidian conservés ; restauration réelle non testée. +- Candidat persistant privé : image de données ext4 2 Gio, profils et réglages Gaia + conservés, identité/comptes système préservés et suppression du watchdog de dix minutes. +- Deux démarrages successifs de la même image v8 : 9 contrôles Gaia et 9 contrôles + système réussis par boot. Diagnostic séparé : 15 contrôles avec la même interface. +- Installation limitée à `boot_a` et restauration de l’original préparées et testées + localement ; aucun flash effectué. Les limites matérielles restent documentées. +- Démarrage normal sans ouverture automatique de Paramètres/Horloge ni défilement + de l’accueil. Ces gestes appartiennent désormais au mode explicite `--diagnostics`. +- Plein écran établi avant le chargement de Gaia ; décorations natives désactivées. + Sur le Pixel testé : 360 × 740 pixels CSS, facteur 3, dimensions stables et accueil + inchangé pendant les cinq relevés après démarrage. +- v13 testé en RAM : huit contrôles de démarrage et neuf contrôles système réussis. + Les rapports distinguent cette vérification du démarrage des tests interactifs. +- Diagnostic v14 séparé : treize contrôles interactifs réussis, dont le panneau + matériel ; ouverture et scan Wi-Fi vérifiés. Même interface que le v13 normal. +- Les images de diagnostic interactif ne peuvent pas devenir la sélection normale + du lanceur. L’ancienne sélection v13 est conservée dans son historique. +- Modem enregistré en UMTS pendant l’essai ; lecture audio réussie et signal du + microphone mesuré sans conserver d’enregistrement. Cela ne qualifie pas encore + l’audio en appel, la caméra ou la veille. +- Construction du ramdisk indépendante de la langue du PC : analyse `readelf` en + locale C pour conserver le chargeur dynamique ARM64 et ses dépendances. + +Les six partitions boot/DTBO/vbmeta A/B sont inchangées. Le build reste un essai +privé en RAM, **pas une ROM flashable**. Détails : [état de Tundra](tundra/docs/STATUS.md). + + + +## En développement — 25 septembre 2026 + +### Commun · Gaia + +- **Navigation :** bouton Accueil rond ; dégradé noir vers transparent au-dessus du + fond d’écran, fond bleu uni dans les applications. L’appui modifie le cercle + plutôt que de laisser un rectangle bleu sur l’accueil. +- **Défilement :** le dégradé disparaît au bas du panneau de l’accueil ; il revient + quand on remonte. Le changement de panneau Applications / Pages est pris en compte. +- **Typographie :** polices Fira Sans embarquées, avec plusieurs graisses et italiques, + au lieu de dépendre des polices installées sur le système hôte. +- **Informations :** affichage séparé de Vulpes, Gaia, du moteur réellement exécuté, + de la plateforme, du modèle et de l’adaptateur. Le panneau prévoit Tundra et + l’adaptation matérielle, maintenant raccordés au backend natif Tundra. +- **Icônes :** reprise du chargement après une première mesure de taille nulle, + partagée entre PC, Android et Tundra. Le correctif était auparavant limité à Android. +- **Maintenance :** ce journal centralise les évolutions des trois plateformes. + +### PC · Validé localement + +- Chargement d’une icône initialisée à taille nulle puis rendue visible : testé. +- Navigation Accueil / Paramètres / retour Accueil, fond transparent en bas de page, + dégradé en haut et contrôle des permissions testés dans un profil isolé. +- Panneau Informations testé en français et en anglais, sans anciennes lignes en double. +- Le moteur demeure Gecko **156.0.1** ; ces changements d’interface ne recompilent pas Gecko. + +### Android · Sources adaptées, Java compilé + +- Même comportement de navigation prévu par le pont GeckoView / Android : barre + superposée sur l’accueil, place réservée dans les applications et le navigateur. +- État de navigation conservé lors de la recréation de l’activité Android. +- Compilation Java réussie. **Aucun nouvel APK livré pour ces changements** et + comportement visuel encore à retester sur Android. + +### Tundra / Sargo · Essais privés en RAM + +- Build réseau v6 devenu référence du lanceur après 13 contrôles Gaia et 9 + contrôles système ; retour automatique et six partitions inchangées vérifiés. +- Backend Tundra distinct du PC : modèle Pixel 3a, adaptation Sargo et version + native affichés sans annoncer une caméra ou une téléphonie non raccordées. +- Batterie et charge lues sur le matériel ; panneau d’état bilingue dans Gaia. + 13 contrôles Gaia réussis sur l’essai réseau, dont ce nouveau panneau. +- Wi-Fi réel testé : scan, connexion, IPv4, DNS et page HTTPS sous Gecko. + Ajout de `pd-mapper`, désactivation d’IPA non initialisé et du sondage caméra + prématuré ; 9 contrôles système passent, sans Oops noyau. La sélection du réseau + dans Gaia reste à raccorder ; les identifiants du test restent en RAM. +- Configuration des services embarquable dans le petit ramdisk : pas de nouvelle + copie des 3 Gio pour chaque ajustement de démarrage. Partitions modem protégées + en utilisant des copies NV en RAM pour les essais Qualcomm. + +- Nouvelle image système ext4 contenant le runtime ARM64, les HAL de référence, + Phoc, Gecko et Gaia. Le runtime n’est plus chargé depuis l’arborescence de travail + Droidian, ni le HAL depuis son ancien fichier `/var/lib/lxc/android/`. +- Démarrage du second essai vérifié sur Pixel 3a : PID 1 Tundra, services HAL, + compositeur et Gaia ; **7 contrôles Gaia et 8 contrôles système réussis**. +- Capture physique de l’accueil contrôlée : icônes présentes, Fira Sans chargée, + bouton rond et dégradé visibles. Gecko **156.0.1 ARM64**, WebRender matériel. +- Petits changements d’interface embarqués dans le ramdisk d’essai : inutile de + retransférer les 3 Gio du système pour chaque correction CSS/JavaScript. +- Copie des sources de construction conservée avec les nouvelles images, vérifiée + par empreintes, pour permettre de les retester après l’évolution du code courant. +- Le dernier essai (`sargo-system-boot-20260925-v5`) valide **10 contrôles Gaia et + 8 contrôles système**, dont icônes, police chargée, fond à l’appui, bas de page + et retour depuis une application. Capture physique contrôlée ; référence locale + du lanceur mise à jour. Aucun APK publié ni téléphone flashé. + +**Limites :** stockage installé encore utilisé comme support du fichier système ; +modifications perdues à l’arrêt ; retour automatique au système conservé après dix +minutes. Aucun flash ni remplacement de Droidian. Le redémarrage à chaud du +compositeur a échoué lors d’un essai ; le démarrage à froid fonctionne. Réseau, +modem, audio, caméra et veille restent à intégrer ou qualifier. Ce n’est pas une +ROM publique installable ni un téléphone quotidien validé. + +Détails : [image système](tundra/docs/SYSTEM-IMAGE-SARGO.md), +[état Tundra](tundra/docs/STATUS.md), [parité des plateformes](docs/PARITE-BUREAU-ANDROID.md). + + + +## 2.7-preview.7 — APK construit, référence locale + +### Commun / PC / Android + +- Correction de l’autorisation interne d’ouverture du capteur sur PC et de la + reprise après la première demande de permission CAMERA sur Android. +- Messages Photo distincts pour un refus, un capteur absent ou un échec d’ouverture. +- Version produit commune **2.7-preview.7**, Gaia **2.7.0** ; numéros du moteur séparés. +- Gecko PC **156.0.1**, GeckoView Android **156.0** : l’écart réel de correctif est + affiché, sans renommer artificiellement l’un des moteurs. + +Détails : [Photo et Galerie](docs/CAMERA-GALERIE.md), [README Android](android/README.md). + + + +## 2.7-preview.6 — APK construit + +### Commun / PC / Android + +- Portage de la véritable application Photo Gaia : aperçu et capture JPEG via + webcam sur PC ou capteur via GeckoView sur Android. +- Photos enregistrées dans Galerie Gaia ; import de médias par le sélecteur du système. +- Commandes non prises en charge désactivées : notamment flash et vidéo ; changement + de caméra selon les capteurs disponibles. + + + +## 2.7-preview.5 — APK construit + +### Commun + +- Versions centralisées dans `release.json`, séparation produit / Gaia / moteur. +- Interface des fonctions natives partagée entre PC et Android ; contrôle du contenu + commun réellement embarqué dans l’APK avant distribution. + +### PC + +- Intégration des panneaux de réglages Linux disponibles, import de médias et vCard, + notifications du bureau et capture webcam de test. + +### Android + +- Même interface Gaia et commandes natives que le PC, adaptées aux capacités Android. +- Tests de retour au premier plan et de récupération après arrêt du processus de rendu. + + + +## 2.7-156.0.2 — APK historique construit (révision 4) + +Ce nom historique mélangeait révision du produit et version du moteur ; il ne +signifie pas que l’APK utilisait Gecko 156.0.2. Le moteur Android était Gecko 156.0. + +### Android + +- Reprise de l’activité et des sessions pour limiter les écrans blancs au retour dans Vulpes. +- Accès aux réglages Android, au volume et à la luminosité de la fenêtre. +- Import de médias et contacts, notifications natives avec permissions. +- Transfert au composeur, à SMS et à Horloge Android ; aucun envoi silencieux ni + remplacement des applications système Téléphone/SMS. +- Capture initialement déléguée à l’application photo Android, avant le portage de Photo Gaia. + + + +## 2.7-156.0.1 — APK publié (révision 3) + +- Nom **Vulpes OS - Preview** et icône du projet. +- Correctif du fond d’écran clignotant inclus. +- Numérotation historique conservée ; ne pas en déduire le patch du moteur Android. + +[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1) + + + +## 2.7-preview.2 — APK publié + +- Correction du fond d’écran clignotant dans la Preview Android. + +[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.2) + + + +## 2.7-preview.1 — APK publié + +- Première Preview Android embarquant Gaia et GeckoView. +- Version archivée ; clignotement du fond d’écran connu. + +[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.1) + + + +## Étapes antérieures — rétrospective technique + +Ces étapes ne correspondent pas à de nouvelles releases publiques datées. + +### Gaia / PC + +- Constitution de Vulpes à partir de Gaia historique ; identité visuelle, fond + d’écran, traductions et travail sur les informations système. +- Portage expérimental Gecko 45 → 52, puis séparation de Gaia, des services et du + moteur pour l’hôte Gecko récent. +- Réimplémentation des API nécessaires : réglages, registre d’applications, + contacts locaux, messages, alarmes, stockage et communications entre applications. +- Adaptation des composants visuels anciens, des interactions tactiles simulées, + du navigateur et des applications ; essais d’applications Firefox OS archivées. +- Outillage de mise à jour Gecko avec vérifications, moteur candidat et retour + arrière explicite. Il détecte les incompatibilités, sans les réparer automatiquement. +- Lancement et mise à jour de Gaia sans recompilation complète du moteur. + +### Tundra / Sargo + +- Inventaire du Pixel 3a, sauvegardes des partitions de boot et tests USB de diagnostic. +- Tests QEMU, puis Gecko ARM64 dans Droidian et sous un compositeur dédié. +- Racine distincte, services HAL sélectionnés et PID 1 Tundra ; essais graphiques + temporaires par Fastboot avec secours SSH et retour automatique. +- Noyau de référence **4.9-124-google-sargo**, adaptation **Android 9 / API 28**. diff --git a/README.fr.md b/README.fr.md index 455b266c..37726606 100644 --- a/README.fr.md +++ b/README.fr.md @@ -7,7 +7,7 @@ Gecko bureau 156.0.1 Expérimental

-

🌐 Le projet · 📦 Téléchargements · 📝 Changelog · 🐞 Signaler un bug

+

🌐 Le projet · 📦 Téléchargements · 📝 Changelog · 🐞 Signaler un bug

## 🦊 Le Web comme interface diff --git a/README.md b/README.md index 62c3a23a..5c5d5f3d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Desktop Gecko 156.0.1 Experimental

-

🌐 Website · 📦 Downloads · 📝 Changelog · 🐞 Report a bug

+

🌐 Website · 📦 Downloads · 📝 Changelog · 🐞 Report a bug

## 🦊 The Web as your interface diff --git a/tools/prepare-publication.py b/tools/prepare-publication.py index 594abba2..3fecfa62 100644 --- a/tools/prepare-publication.py +++ b/tools/prepare-publication.py @@ -11,7 +11,7 @@ import subprocess ROOT = Path(__file__).resolve().parents[1] SOURCE_DIRS = {'adapters','android','assets','contracts','gaia','host','overrides','services','tests','tools','tundra'} ROOT_FILES = {'.gitignore','.nvmrc','.prettierignore','.prettierrc.json','LICENSE','NOTICE', - 'README.md','README.fr.md','engine-lock.json','package.json','package-lock.json', + 'README.md','README.fr.md','CHANGELOG.md','CHANGELOG.en.md','engine-lock.json','package.json','package-lock.json', 'pyproject.toml','requirements-dev.txt','release.json','changelog.json','run.sh','setup.sh'} INTERNAL = {'VALIDATION.json','candidate-engine.json', 'tools/export-changelog.py','tools/sync-forgejo-changelog.py','tests/test_changelog.py', @@ -25,7 +25,7 @@ def target_name(name): if name in MEDIA:return name if p.parts[0] not in SOURCE_DIRS and name not in ROOT_FILES:return None if 'docs' in p.parts or p.name=='VALIDATION.json':return None - if p.suffix.lower()=='.md' and name not in {'README.md','README.fr.md'}: + if p.suffix.lower()=='.md' and name not in {'README.md','README.fr.md','CHANGELOG.md','CHANGELOG.en.md'}: # License notices are retained verbatim, without publishing internal Markdown. if p.name.lower() in {'license.md','copying.md','notice.md'}:return str(p.with_suffix('')) return None From aeea5ab595206ffc8e2bb48d1a767ce02ea34804 Mon Sep 17 00:00:00 2001 From: Alexandre NOEL <95a7002b-481b-4ef3-b578-f9c421550aa4@localhost> Date: Thu, 8 Oct 2026 18:46:39 +0200 Subject: [PATCH 03/12] Automate bilingual changelog generation before publication --- tools/prepare-publication.py | 3 ++ tools/render-changelog.py | 76 ++++++++++++++++++++++++++++++++++ tools/test-changelog-render.py | 75 +++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 tools/render-changelog.py create mode 100644 tools/test-changelog-render.py diff --git a/tools/prepare-publication.py b/tools/prepare-publication.py index 3fecfa62..9b9dbb03 100644 --- a/tools/prepare-publication.py +++ b/tools/prepare-publication.py @@ -7,6 +7,7 @@ import os from pathlib import Path import re import subprocess +import sys ROOT = Path(__file__).resolve().parents[1] SOURCE_DIRS = {'adapters','android','assets','contracts','gaia','host','overrides','services','tests','tools','tundra'} @@ -56,6 +57,8 @@ def main(): args=parser.parse_args() index=args.index.resolve();normal=Path(subprocess.check_output(['git','rev-parse','--git-path','index'],cwd=ROOT,text=True).strip()).resolve() if index==normal:raise ValueError('Refusing to replace working index') + # Render both languages before selecting blobs for the publication tree. + subprocess.run([sys.executable, str(ROOT/'tools/render-changelog.py')], cwd=ROOT, check=True) selected=select();env=dict(os.environ,GIT_INDEX_FILE=str(index)) index.parent.mkdir(parents=True,exist_ok=True) subprocess.run(['git','read-tree','--empty'],cwd=ROOT,env=env,check=True) diff --git a/tools/render-changelog.py b/tools/render-changelog.py new file mode 100644 index 00000000..866e7ac1 --- /dev/null +++ b/tools/render-changelog.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Generate the public FR/EN changelogs from changelog.json (standard library only).""" +import argparse +import json +import os +from pathlib import Path +import re +import tempfile + +ROOT = Path(__file__).resolve().parents[1] +OUTPUTS = {"fr": "CHANGELOG.md", "en": "CHANGELOG.en.md"} + + +def render(source): + if source.get("schema") != 1 or not isinstance(source.get("entries"), list): + raise ValueError("Unsupported changelog schema") + ids = set() + for entry in source["entries"]: + identifier = entry["id"] + if not re.fullmatch(r"[A-Za-z0-9_.-]+", identifier) or identifier in ids: + raise ValueError("Invalid or duplicate entry id: " + identifier) + ids.add(identifier) + if not ids: + raise ValueError("The changelog must not be empty") + documents = {} + for language, filename in OUTPUTS.items(): + preamble = source["preamble"][language].strip() + if not preamble.startswith("# "): + raise ValueError("Missing preamble for " + language) + sections = [preamble] + for entry in source["entries"]: + body = entry["text"][language].strip() + if not body.startswith("## "): + raise ValueError("Missing heading: " + entry["id"] + "/" + language) + sections.append('\n\n' + body) + documents[filename] = "\n\n".join(sections) + "\n" + return documents + + +def generate(root=ROOT, check=False): + documents = render(json.loads((root / "changelog.json").read_text(encoding="utf-8"))) + changed = [] + for filename, content in documents.items(): + path = root / filename + if path.exists() and path.read_text(encoding="utf-8") == content: + continue + changed.append(filename) + if check: + continue + descriptor, temporary = tempfile.mkstemp(prefix=".changelog-", dir=root) + try: + with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as stream: + stream.write(content) + os.chmod(temporary, 0o644) + os.replace(temporary, path) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + return changed + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true", help="Fail if generated files are out of date") + args = parser.parse_args() + try: + changed = generate(check=args.check) + except (ValueError, KeyError, TypeError, AttributeError) as error: + parser.exit(2, "Invalid changelog: " + str(error) + "\n") + if changed: + print(("Out of date: " if args.check else "Generated: ") + ", ".join(changed)) + return 1 if args.check and changed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/test-changelog-render.py b/tools/test-changelog-render.py new file mode 100644 index 00000000..9bc0546f --- /dev/null +++ b/tools/test-changelog-render.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Regression checks for generated changelogs and publication integration.""" +import copy +import importlib.util +import json +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location("render_changelog", ROOT / "tools/render-changelog.py") +renderer = importlib.util.module_from_spec(spec) +spec.loader.exec_module(renderer) + + +class ChangelogTests(unittest.TestCase): + def setUp(self): + self.source = json.loads((ROOT / "changelog.json").read_text()) + + def test_no_translation_or_entry_is_lost(self): + documents = renderer.render(self.source) + for language, filename in renderer.OUTPUTS.items(): + for entry in self.source["entries"]: + self.assertIn(entry["text"][language].strip(), documents[filename]) + self.assertIn('', documents[filename]) + + def test_duplicate_ids_and_missing_translation_are_rejected(self): + duplicate = copy.deepcopy(self.source) + duplicate["entries"].append(duplicate["entries"][0]) + with self.assertRaises(ValueError): + renderer.render(duplicate) + del self.source["entries"][0]["text"]["en"] + with self.assertRaises(KeyError): + renderer.render(self.source) + + def test_check_mode_does_not_overwrite_files(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "changelog.json").write_text(json.dumps(self.source)) + (root / "CHANGELOG.md").write_text("Old content") + self.assertEqual(len(renderer.generate(root, check=True)), 2) + self.assertEqual((root / "CHANGELOG.md").read_text(), "Old content") + renderer.generate(root) + self.assertEqual(renderer.generate(root, check=True), []) + + def test_publication_renders_updates_without_touching_working_index(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "tools").mkdir() + for name in ["render-changelog.py", "prepare-publication.py"]: + shutil.copy2(ROOT / "tools" / name, root / "tools" / name) + for name in ["LICENSE", "NOTICE", "README.md", "README.fr.md"]: + (root / name).write_text("Publication fixture\n") + (root / "changelog.json").write_text(json.dumps(self.source)) + subprocess.run(["git", "init", "-q", str(root)], check=True) + subprocess.run(["git", "add", "."], cwd=root, check=True) + before = (root / ".git/index").read_bytes() + self.source["entries"][0]["text"]["fr"] += "\n\nPublication integration check." + (root / "changelog.json").write_text(json.dumps(self.source)) + command = [sys.executable, str(root / "tools/prepare-publication.py"), + "--index", str(root / ".git/publication-index"), + "--report", str(root / ".git/publication-report.json")] + subprocess.run(command, cwd=root, check=True, capture_output=True) + report = json.loads((root / ".git/publication-report.json").read_text()) + published = subprocess.check_output(["git", "show", report["tree"] + ":CHANGELOG.md"], cwd=root, text=True) + self.assertIn("Publication integration check.", published) + self.assertIn("CHANGELOG.en.md", report["manifest"]) + self.assertEqual(before, (root / ".git/index").read_bytes()) + + +if __name__ == "__main__": + unittest.main() From be551ac49f0044f7d92aae45f704ee6fd4db0e2f Mon Sep 17 00:00:00 2001 From: Alexandre NOEL <95a7002b-481b-4ef3-b578-f9c421550aa4@localhost> Date: Thu, 8 Oct 2026 19:03:02 +0200 Subject: [PATCH 04/12] Simplify French and English READMEs and refresh project badges --- README.fr.md | 115 +++++++++++++++++++-------------------------------- README.md | 115 +++++++++++++++++++-------------------------------- 2 files changed, 84 insertions(+), 146 deletions(-) diff --git a/README.fr.md b/README.fr.md index 37726606..7f88d812 100644 --- a/README.fr.md +++ b/README.fr.md @@ -1,45 +1,43 @@ -

🇬🇧 English · 🇫🇷 Français

-

Logo renard de Vulpes OS

+

English · Français

+

Vulpes OS

Vulpes OS

-

L'esprit de Firefox OS. Un Gecko moderne. Une base Gaia commune.
Un projet communautaire pour retrouver une expérience mobile fondée sur le Web, sur PC, Android et téléphone natif.

+

Firefox OS fork · Open source

+

-Vulpes Preview 17 -Gecko bureau 156.0.1 -Expérimental +Vulpes: 2.7 Preview 17 +Gaia: 2.7 +Gecko: 156.0.1 +Tundra: 0.1.0 +

+

+Android APK: Available +Desktop: Available +Native image: In development +Pixel 3a: Bootable and usable

-

🌐 Le projet · 📦 Téléchargements · 📝 Changelog · 🐞 Signaler un bug

-## 🦊 Le Web comme interface +[Site](https://vulpes-os.org/?lang=fr) · [Téléchargements](https://nnsprod.com/git/oversu/VulpesOS/releases) · [Changelog](CHANGELOG.md) · [Signaler un bug](https://nnsprod.com/git/oversu/VulpesOS/issues) -Accueil, paramètres, appareil photo, galerie, messages : **Gaia reste l'interface**, composée de HTML, CSS et JavaScript. Vulpes adapte les API attendues par ces applications Firefox OS à un moteur Gecko moderne et à des services propres à chaque plateforme. +Vulpes OS reprend Firefox OS pour faire fonctionner **Gaia**, son interface et ses applications HTML/CSS/JavaScript, avec un moteur **Gecko** récent. -Ce dépôt contient désormais l'interface commune, l'hôte PC, l'application Android, le runtime Tundra et les outils de développement. C'est une **première préversion pour développeurs**, pas un système mobile terminé. +Je développe Vulpes principalement seul, sous le nom d’OverSu, avec l’aide des personnes qui testent et contribuent au projet. **Vulpes est libre et gratuit, et le restera.** Aucun abonnement ni fonctionnalité payante n’est prévu. -

Capture réelle du bureau Gaia en anglais   Capture réelle des informations système et du moteur Gecko

-

Captures de l'environnement de développement PC. Les fonctionnalités dépendent de la plateforme et du matériel disponible.

+

L’accueil Gaia de Vulpes OS sur ordinateur

+

L’accueil de Gaia dans la version pour ordinateur.

-## 📥 Choisissez votre plateforme +## Essayer Vulpes -| Plateforme | Disponible aujourd'hui | Moteur | -| --- | --- | --- | -| **PC Linux** | Sources, fenêtre Gaia au format téléphone, développement et tests d'applications | Gecko **156.0.1** | -| **APK Android** | Preview installable ; Android et ses permissions restent en dessous | GeckoView **156.0** | -| **Pixel 3a · Sargo** | Sources Tundra ; build natif de développement testé sur notre appareil | Gecko **156.0.1** ARM64 | +| Plateforme | Comment l’utiliser | +| --- | --- | +| **Linux** | Lancer Gaia depuis les sources pour découvrir l’interface ou développer les applications. | +| **Android** | Installer l’[APK disponible dans les releases](https://nnsprod.com/git/oversu/VulpesOS/releases). Il fonctionne dans Android et ne remplace pas le système du téléphone. | +| **Google Pixel 3a · Sargo** | Faire fonctionner Vulpes comme système natif avec Tundra. L’image à télécharger est encore en préparation ; les fonctions testées et les limites sont décrites dans la [fiche de l’appareil](https://vulpes-os.org/devices/pixel-3a.php?lang=fr). | -### Android — Preview 17 +Sur Pixel 3a, **Tundra démarre directement Gecko et Gaia** et assure leur accès au matériel. Droidian a servi d’environnement de test au début du projet : il n’est pas inclus dans Vulpes OS et Tundra ne repose pas dessus. -Téléchargez l'APK et `SHA256SUMS.txt` depuis [la release Preview 17](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.17). Une installation par-dessus une ancienne Preview Vulpes signée conserve ses données. +### Linux -- Minimum déclaré : **Android 8 / API 26**. Cible : **API 37**. -- APK universel : **ARM64, ARMv7 et x86_64** ; environ **600 Mio**, moteur GeckoView inclus. -- Photo et Galerie utilisent Gaia, avec les permissions et l'import de fichiers Android. -- Les appels et SMS sont transmis aux applications Android ; Vulpes n'est pas l'application Téléphone/SMS par défaut. -- Des parcours sur émulateur Android 16 sont testés. Le minimum SDK déclaré ne signifie pas que tous les appareils Android ont été validés. -- Il s'agit d'un **build de développement signé**, avec débogage Gecko accessible via ADB autorisé, pas d'une application durcie pour la production. - -### Linux — lancer depuis les sources - -Prérequis : Linux x86_64, Python 3, Git et les bibliothèques système nécessaires à Firefox. Sur Debian/Ubuntu : `git python3 libgtk-3-0 libdbus-glib-1-2 libasound2` (les noms de paquets peuvent varier selon la distribution). +Prérequis : Linux x86_64, Git, Python 3 et les bibliothèques système nécessaires à Firefox. ```sh git clone https://nnsprod.com/git/oversu/VulpesOS.git @@ -48,58 +46,29 @@ cd VulpesOS ./run.sh ``` -`setup.sh` télécharge le moteur Mozilla verrouillé et vérifie son empreinte. Le lancement normal ouvre Gaia sans débogueur. Les profils et moteurs téléchargés restent hors Git. +`setup.sh` télécharge le moteur Gecko prévu pour cette version et vérifie son empreinte. -### Pixel 3a — développement natif Tundra +### Compiler l’APK -**Tundra démarre directement Gecko et Gaia**, avec un espace utilisateur Linux et les services matériels Android sélectionnés pour l’appareil. - -Notre image de développement Sargo installée a passé les contrôles de démarrage et de persistance. Des appels réels et des SMS dans les deux sens ont été observés ; les derniers essais couvrent les conversations, les bascules de capteur photo, la capture JPEG pleine résolution, le focus tactile, le zoom numérique et l'arrêt du vibreur. - -**Une installation autonome est maintenant testée localement.** Le paquet Fastboot fournit le système, un espace de données vierge et les composants matériels associés au noyau. Il remplace `userdata`, `boot_a`, `dtbo_a` et `vbmeta_a` : il n’utilise plus les fichiers d’une ancienne installation Droidian et ne nécessite pas de session Android ni d’ADB. Deux démarrages depuis le stockage interne ont passé chacun onze contrôles Gaia et dix contrôles natifs, avec conservation des réglages. - -L’installation demande un **Pixel 3a (Sargo) déverrouillé** et efface toutes les données de `userdata`. Le bootloader et les calibrations propres au téléphone restent en place. Ce candidat réserve actuellement 8 Gio au volume, dont 1 Gio de données modifiables ; l’extension automatique n’est pas encore implémentée. Il a été testé sur notre Pixel, pas sur chaque combinaison de firmware existante. - -**Le paquet autonome n’est pas encore proposé en téléchargement dans cette release.** Les recettes et l’installateur sont préparés ; la revue de provenance et de redistribution des composants matériels reste à terminer. Les sauvegardes et les clés de maintenance du téléphone de développement ne font pas partie du paquet générique. - -## 🛠️ Dernières évolutions des sources · 8 octobre 2026 - -- **Photo :** bascule des capteurs séquencée, libération fiable et transport des images corrigé. Capture avant/arrière, focus, zoom et ouverture dans Galerie testés sur notre Pixel 3a. -- **Contacts :** fiche actualisée après édition, parcours SMS/sélecteur/Téléphone rétablis et sélection d’une photo dans Galerie. Les tests PC couvrent recherche, recadrage, favoris et suppression ; les tests natifs couvrent création, édition et navigation entre applications. -- **Marketplace :** installation d’applications intégrée, permissions restreintes et retour contrôlé vers Gaia. -- **Tundra :** outils de build autonome et de restauration, paquets Bluetooth verrouillés et certificats natifs. La découverte matérielle Bluetooth fonctionne ; l’appairage Gaia et les profils restent à terminer. - -**Ces changements de sources sont postérieurs à l’APK Preview 17 publié.** Les ressources Android sont préparées, mais aucun nouvel APK ni image native contenant tous ces correctifs n’est publié. Le dernier candidat natif est testé en RAM ; l’enregistrement cellulaire reste en cours d’analyse et l’audio des appels dans les deux sens n’est pas encore qualifié. L’historique commun PC/Android/Tundra se trouve dans [`changelog.json`](changelog.json). - -## ✨ Ce que nous partageons - -- **Une base Gaia :** interface et services communs pour PC, Android et Tundra. -- **Les applications d'origine :** Photo, Galerie, Paramètres et Messages conservent leur interface Gaia, reliée aux adaptateurs de chaque plateforme. -- **Du code Web éditable :** les ressources servies sont dans `assets/webapps/`, la compatibilité commune dans `host/`, les remplacements dans `overrides/`. Rechargez ou redémarrez pour tester, sans recompiler Gecko. Certaines sources historiques de `gaia/` nécessitent encore la mise à jour de leur version regroupée. -- **Un moteur remplaçable :** téléchargements verrouillés, contrôle d'empreinte, tests isolés et outils de retour arrière. Une mise à jour du moteur se teste et se sélectionne explicitement ; la compatibilité n'est pas garantie automatiquement. -- **Des versions distinctes :** Vulpes, Gaia, Gecko et adaptation matérielle sont identifiés séparément. GeckoView 156.0 n'est pas présenté comme Gecko bureau 156.0.1. - -## 🧪 Limites actuelles - -Cette Preview contient des adaptateurs matériels incomplets et des comportements hérités à corriger. Sur Sargo, l'aperçu photo tourne autour de 16 images/s ; flash/vidéo, rotation automatique par capteur et suspension complète restent incomplets. L'enregistrement réseau peut être instable et l'audio d'une conversation dans les deux sens reste à qualifier. Un grand dialogue signalé après envoi de SMS est encore en cours d'analyse. - -Sur Android, les intégrations suivent les permissions et les restrictions de cycle de vie du système. Les alarmes Gaia ne sont pas garanties après l'arrêt de Vulpes par Android. Certaines commandes non prises en charge restent visibles mais désactivées. Des applications de développement/test sont incluses. - -Conservez une sauvegarde avant vos essais. Pour signaler un problème, indiquez la plateforme, les versions Vulpes/Gecko et les étapes de reproduction ; retirez des logs les numéros, contacts et autres informations personnelles. - -## 🛠️ Construire l'APK Android +Depuis la racine du dépôt : ```sh cd android ./build-apk.sh ``` -Le script installe les outils verrouillés, prépare les ressources Gaia communes et construit un APK de développement signé dans `android/dist/`. L'acceptation des licences SDK Android peut être nécessaire. Un clone neuf génère sa propre clé de signature : il ne pourra pas mettre à jour un APK signé avec la clé privée du projet. +L’APK de développement est créé dans `android/dist/`. Une compilation personnelle utilise sa propre clé de signature : elle ne peut pas mettre à jour un APK signé avec la clé du projet. -## 📜 Remerciements et licences +## Participer -Vulpes OS est un projet communautaire d'**OverSu / NNS Production**, inspiré de Firefox OS. Merci à **Mozilla, aux contributeurs Gaia, à Capyloon, Droidian, UBports, libhybris, Debian, AOSP** et aux communautés qui font vivre les systèmes mobiles ouverts. +Vous pouvez tester Vulpes, [signaler un problème](https://nnsprod.com/git/oversu/VulpesOS/issues), améliorer les traductions ou proposer du code. Pour un bug, indiquez la plateforme, la version utilisée et les étapes pour le reproduire. -Les nouveaux fichiers Vulpes utilisent **MPL 2.0**. Gaia conserve **Apache 2.0** ; les composants hérités conservent leurs licences et mentions. Voir [LICENSE](LICENSE), [NOTICE](NOTICE) et [la licence Gaia](gaia/LICENSE). Les forks doivent conserver les mentions d'attribution et de licence applicables. +Les avancées et les comptes rendus d’essais sont dans le [changelog](CHANGELOG.md). Si vous souhaitez contribuer aux frais du projet, la page [Me soutenir](https://vulpes-os.org/support.php?lang=fr) explique à quoi sert ce soutien. -Vulpes OS est indépendant de Mozilla et n'est pas une version officielle de Firefox OS. +## Crédits et licences + +Merci à Mozilla, aux contributeurs de Gaia, à Capyloon, Droidian, UBports, libhybris, Debian et AOSP pour le code, les outils et les travaux qui ont aidé le projet. + +Les nouveaux fichiers Vulpes sont sous **MPL 2.0**. Gaia conserve sa licence **Apache 2.0** ; les autres composants conservent leurs licences et mentions. Voir [LICENSE](LICENSE), [NOTICE](NOTICE) et [la licence de Gaia](gaia/LICENSE). + +Vulpes OS est un projet indépendant d’**OverSu / NNS Production**, sans affiliation avec Mozilla. diff --git a/README.md b/README.md index 5c5d5f3d..8cb26789 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,43 @@ -

🇬🇧 English · 🇫🇷 Français

-

Vulpes OS fox logo

+

English · Français

+

Vulpes OS

Vulpes OS

-

The spirit of Firefox OS. A modern Gecko. One shared Gaia.
A community project bringing a web-based phone experience to desktop, Android and native hardware.

+

Firefox OS fork · Open source

+

-Vulpes Preview 17 -Desktop Gecko 156.0.1 -Experimental +Vulpes: 2.7 Preview 17 +Gaia: 2.7 +Gecko: 156.0.1 +Tundra: 0.1.0 +

+

+Android APK: Available +Desktop: Available +Native image: In development +Pixel 3a: Bootable and usable

-

🌐 Website · 📦 Downloads · 📝 Changelog · 🐞 Report a bug

-## 🦊 The Web as your interface +[Website](https://vulpes-os.org/?lang=en) · [Downloads](https://nnsprod.com/git/oversu/VulpesOS/releases) · [Changelog](CHANGELOG.en.md) · [Report a bug](https://nnsprod.com/git/oversu/VulpesOS/issues) -Home screen, settings, camera, gallery, messages: **Gaia remains the interface**, built from HTML, CSS and JavaScript. Vulpes adapts the APIs that these Firefox OS applications expect to a modern Gecko engine and platform-specific services. +Vulpes OS picks up where Firefox OS left off, adapting **Gaia**, its interface and HTML/CSS/JavaScript applications, to run on a recent **Gecko** engine. -This repository now contains the shared interface, desktop host, Android application, Tundra runtime and development tools. This is an **early developer preview**, not a finished phone operating system. +I’m OverSu, and I develop Vulpes mostly on my own, with help from people who test and contribute to the project. **Vulpes is free and open source, and will stay that way.** There are no plans for subscriptions or paid features. -

Actual desktop screenshot of the Gaia home screen in English   Actual desktop screenshot of device information showing the Gecko engine

-

Desktop development screenshots. Capabilities depend on the platform and available hardware.

+

The Vulpes OS Gaia home screen running on desktop

+

The Gaia home screen in the desktop version.

-## 📥 Choose your platform +## Try Vulpes -| Platform | Available today | Engine | -| --- | --- | --- | -| **Linux desktop** | Source checkout, phone-sized Gaia window, development and app tests | Gecko **156.0.1** | -| **Android APK** | Installable Preview; keeps Android and its permissions underneath | GeckoView **156.0** | -| **Pixel 3a · Sargo** | Tundra sources; native development build tested on our device | Gecko **156.0.1** ARM64 | +| Platform | How to use it | +| --- | --- | +| **Linux** | Run Gaia from source to explore the interface or develop applications. | +| **Android** | Install the [APK from the releases page](https://nnsprod.com/git/oversu/VulpesOS/releases). It runs as an Android application and does not replace your phone’s operating system. | +| **Google Pixel 3a · Sargo** | Run Vulpes as a native operating system with Tundra. A downloadable image is still being prepared; see the [device page](https://vulpes-os.org/devices/pixel-3a.php?lang=en) for tested features and known limitations. | -### Android — Preview 17 +On the Pixel 3a, **Tundra boots Gecko and Gaia directly** and provides access to the hardware. Droidian was used as a testing environment early in development. It is not included in Vulpes OS, and Tundra is not based on it. -Download the APK and `SHA256SUMS.txt` from [the Preview 17 release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.17). Installing over an earlier signed Vulpes Preview preserves its data. +### Linux -- Minimum declared: **Android 8 / API 26**. Target: **API 37**. -- Universal APK: **ARM64, ARMv7 and x86_64**; approximately **600 MiB** including GeckoView. -- Camera and Gallery use Gaia, with Android permissions and file import. -- Calls and SMS hand off to Android's applications; Vulpes is not the default SMS or phone app. -- Android 16 emulator workflows are tested. Minimum SDK support is not a claim that every Android device is validated. -- This is a signed **development build** with Gecko debugging available through authorized ADB, not a production-hardened app. - -### Linux — run from source - -Linux x86_64, Python 3, Git and the system libraries needed by Firefox are required. On Debian/Ubuntu, install `git python3 libgtk-3-0 libdbus-glib-1-2 libasound2` (package names may differ by distribution). +You’ll need Linux x86_64, Git, Python 3 and the system libraries required by Firefox. ```sh git clone https://nnsprod.com/git/oversu/VulpesOS.git @@ -48,58 +46,29 @@ cd VulpesOS ./run.sh ``` -`setup.sh` downloads the pinned Mozilla engine and verifies its checksum. Normal startup opens the Gaia interface without a debugger. Profiles and downloaded engines stay outside Git. +`setup.sh` downloads the Gecko engine selected for this version and verifies its checksum. -### Pixel 3a — native Tundra development +### Build the APK -**Tundra boots Gecko and Gaia directly**, using Linux userspace and the Android hardware services selected for the device. - -Our installed Sargo development image has passed startup and persistence checks. Real calls and two-way SMS have been observed; the latest checks cover conversation rendering, camera sensor switching, full-resolution JPEG capture, touch focus, digital zoom and bounded vibration. - -**A standalone installation has now been tested locally.** The Fastboot package provides the system, fresh writable storage and the hardware components paired with the kernel. It replaces `userdata`, `boot_a`, `dtbo_a` and `vbmeta_a`: startup no longer uses files from an old Droidian installation and requires neither an Android session nor ADB. Two boots from internal storage passed eleven Gaia and ten native checks each and retained settings. - -Installation requires an **unlocked Pixel 3a (Sargo)** and erases all existing `userdata`. The bootloader and device-specific calibration remain in place. This candidate currently reserves an 8 GiB volume, including 1 GiB of writable data; automatic expansion is not implemented yet. It has been tested on our Pixel, not every existing firmware combination. - -**The standalone package is not yet available as a download in this release.** Build recipes and the installer are prepared; hardware component provenance and redistribution review are still pending. Development-phone backups and maintenance keys are excluded from the generic package. - -## 🛠️ Latest source improvements · 8 October 2026 - -- **Camera:** serialized sensor switching, reliable release and binary photo transport. Front/rear capture, focus, zoom and Gallery opening tested on our Pixel 3a. -- **Contacts:** refreshed details after editing, restored SMS/number-picker/dialer flows, and Gallery photo selection. Desktop tests cover search, cropping, favourites and deletion; native tests cover creation, editing and cross-app navigation. -- **Marketplace:** integrated application installation with permission restrictions and controlled return to Gaia. -- **Tundra:** standalone build/recovery tools, pinned Bluetooth runtime packages and native trusted certificates. Bluetooth hardware discovery works; Gaia pairing and profiles remain unfinished. - -**These are source updates, newer than the published Preview 17 APK.** Updated Android assets are prepared, but no new APK or native image containing all these fixes is published. The latest native candidate is tested in RAM; cellular registration remains under investigation and two-way call audio is not yet qualified. See [`changelog.json`](changelog.json) for the shared desktop/Android/Tundra history. - -## ✨ What is shared - -- **One Gaia base:** interface and common services serve desktop, Android and Tundra. -- **Original applications:** Camera, Gallery, Settings and Messages keep their Gaia UI, backed by platform adapters. -- **Editable web code:** change served assets in `assets/webapps/`, shared compatibility code in `host/`, and replacements in `overrides/`. Restart/reload to test; no Gecko rebuild is needed for these changes. Some historical `gaia/` sources still need their bundled counterparts updated. -- **A replaceable engine:** pinned downloads, checksum verification, isolated compatibility checks and rollback tools. An engine update is tested and explicitly selected; compatibility is not guaranteed automatically. -- **Separate version numbers:** Vulpes release, Gaia, Gecko and hardware adaptation are reported independently. GeckoView 156.0 is not presented as desktop Gecko 156.0.1. - -## 🧪 Current limits - -This preview contains incomplete hardware adapters and legacy application behavior. On Sargo, camera preview is around 16 fps; flash/video, automatic sensor rotation and full suspend remain incomplete. Network registration can be unstable and two-way call audio still needs end-to-end qualification. A reported large dialog after SMS sending remains under investigation. - -On Android, platform integration follows Android permissions and lifecycle restrictions. Gaia alarms are not guaranteed after Android stops Vulpes. Unsupported controls may remain visible but disabled. Development/test applications are included. - -Keep a backup before testing. Include the platform, Vulpes/Gecko versions and reproduction steps when reporting a problem; remove phone numbers, contacts and other personal information from logs. - -## 🛠️ Build the Android APK +From the repository root: ```sh cd android ./build-apk.sh ``` -The script installs the pinned toolchain, packages the shared Gaia resources and builds a signed development APK in `android/dist/`. Android SDK licenses may need acceptance. A fresh checkout generates its own signing key; it cannot update an APK signed by the project's private release key. +The development APK is created in `android/dist/`. A personal build uses its own signing key, so it cannot update an APK signed with the project’s key. -## 📜 Credits and licenses +## Contribute -Vulpes OS is a community project by **OverSu / NNS Production**, inspired by Firefox OS. Thanks to **Mozilla, Gaia contributors, Capyloon, Droidian, UBports, libhybris, Debian, AOSP** and the communities keeping open mobile systems alive. +You can test Vulpes, [report an issue](https://nnsprod.com/git/oversu/VulpesOS/issues), improve translations or contribute code. When reporting a bug, include your platform, the version you’re using and the steps to reproduce it. -New Vulpes code uses **MPL 2.0**. Gaia retains **Apache 2.0** and inherited components retain their own licenses and notices. See [LICENSE](LICENSE), [NOTICE](NOTICE) and [Gaia's license](gaia/LICENSE). Preserve the applicable attribution and license notices in forks. +Development updates and test results are in the [changelog](CHANGELOG.en.md). If you’d like to help cover project costs, the [Support me](https://vulpes-os.org/support.php?lang=en) page explains what your contribution supports. -Vulpes OS is independent of Mozilla and is not an official Firefox OS release. +## Credits and licenses + +Thanks to Mozilla, Gaia contributors, Capyloon, Droidian, UBports, libhybris, Debian and AOSP for the code, tools and work that have helped this project. + +New Vulpes code is licensed under **MPL 2.0**. Gaia retains its **Apache 2.0** license; other components retain their own licenses and notices. See [LICENSE](LICENSE), [NOTICE](NOTICE) and [Gaia’s license](gaia/LICENSE). + +Vulpes OS is an independent project by **OverSu / NNS Production**, with no affiliation with Mozilla. From 7839ff24c5e55ca49993d61f81eaf94eec724010 Mon Sep 17 00:00:00 2001 From: Alexandre NOEL <95a7002b-481b-4ef3-b578-f9c421550aa4@localhost> Date: Thu, 8 Oct 2026 21:53:22 +0200 Subject: [PATCH 05/12] Add Settings screenshot to changelogs and fix README language links --- CHANGELOG.en.md | 3 +++ CHANGELOG.md | 3 +++ README.fr.md | 2 +- README.md | 2 +- changelog.json | 4 ++-- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index 03b009cd..d88e6361 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -9,6 +9,9 @@ This journal covers visible changes and major technical milestones. and “Published” to a public repository release. Shared source changes reach installed APKs and phone images only after those packages are updated. +

Gaia Settings, system information screen

+

Gaia Settings · screenshot from the desktop version.

+ ## 8 October 2026 — Public source update diff --git a/CHANGELOG.md b/CHANGELOG.md index cab05b3a..c797ca7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ Les rapports détaillés restent dans la documentation de chaque plateforme. un paquet de test, et « Publié » une release accessible dans le dépôt. Un correctif commun n’apparaît dans un APK installé ou une image téléphone qu’après leur mise à jour. +

Les paramètres de Gaia, écran Informations système

+

Paramètres de Gaia · capture de la version pour ordinateur.

+ ## 8 octobre 2026 — Mise à jour des sources publiques diff --git a/README.fr.md b/README.fr.md index 7f88d812..9362c19c 100644 --- a/README.fr.md +++ b/README.fr.md @@ -1,4 +1,4 @@ -

English · Français

+

English · Français

Vulpes OS

Vulpes OS

Firefox OS fork · Open source

diff --git a/README.md b/README.md index 8cb26789..cf6afb8c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

English · Français

+

English · Français

Vulpes OS

Vulpes OS

Firefox OS fork · Open source

diff --git a/changelog.json b/changelog.json index 4efc193f..3626f5a6 100644 --- a/changelog.json +++ b/changelog.json @@ -1,8 +1,8 @@ { "schema": 1, "preamble": { - "fr": "# Journal des évolutions — Vulpes OS\n\n[English](CHANGELOG.en.md)\n\nUne base Gaia, trois plateformes : **PC**, **Android (APK)** et **Tundra / Sargo**.\nCe journal décrit les changements visibles et les étapes techniques importantes.\nLes rapports détaillés restent dans la documentation de chaque plateforme.\n\n**Statuts :** « En développement » concerne les sources locales, « APK construit »\nun paquet de test, et « Publié » une release accessible dans le dépôt. Un correctif\ncommun n’apparaît dans un APK installé ou une image téléphone qu’après leur mise à jour.", - "en": "# Changelog — Vulpes OS\n\n[Français](CHANGELOG.md)\n\nOne Gaia base, three platforms: **desktop**, **Android (APK)** and **Tundra / Sargo**.\nThis journal covers visible changes and major technical milestones.\n\n**Status:** “In development” refers to local sources, “APK built” to a test package,\nand “Published” to a public repository release. Shared source changes reach installed\nAPKs and phone images only after those packages are updated." + "fr": "# Journal des évolutions — Vulpes OS\n\n[English](CHANGELOG.en.md)\n\nUne base Gaia, trois plateformes : **PC**, **Android (APK)** et **Tundra / Sargo**.\nCe journal décrit les changements visibles et les étapes techniques importantes.\nLes rapports détaillés restent dans la documentation de chaque plateforme.\n\n**Statuts :** « En développement » concerne les sources locales, « APK construit »\nun paquet de test, et « Publié » une release accessible dans le dépôt. Un correctif\ncommun n’apparaît dans un APK installé ou une image téléphone qu’après leur mise à jour.\n\n

\"Les

\n

Paramètres de Gaia · capture de la version pour ordinateur.

", + "en": "# Changelog — Vulpes OS\n\n[Français](CHANGELOG.md)\n\nOne Gaia base, three platforms: **desktop**, **Android (APK)** and **Tundra / Sargo**.\nThis journal covers visible changes and major technical milestones.\n\n**Status:** “In development” refers to local sources, “APK built” to a test package,\nand “Published” to a public repository release. Shared source changes reach installed\nAPKs and phone images only after those packages are updated.\n\n

\"Gaia

\n

Gaia Settings · screenshot from the desktop version.

" }, "entries": [ { From e14e2b00e113659bc3ea8f587843a95e7f249b87 Mon Sep 17 00:00:00 2001 From: Alexandre NOEL <95a7002b-481b-4ef3-b578-f9c421550aa4@localhost> Date: Thu, 8 Oct 2026 22:05:49 +0200 Subject: [PATCH 06/12] Show Gaia home and Settings screenshots in both changelogs --- CHANGELOG.en.md | 7 +++++-- CHANGELOG.md | 7 +++++-- changelog.json | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index d88e6361..456a2a18 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -9,8 +9,11 @@ This journal covers visible changes and major technical milestones. and “Published” to a public repository release. Shared source changes reach installed APKs and phone images only after those packages are updated. -

Gaia Settings, system information screen

-

Gaia Settings · screenshot from the desktop version.

+

+The Gaia home screen +Gaia Settings, system information screen +

+

Gaia home screen and Settings · screenshots from the desktop version.

diff --git a/CHANGELOG.md b/CHANGELOG.md index c797ca7c..2ba8c48e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,11 @@ Les rapports détaillés restent dans la documentation de chaque plateforme. un paquet de test, et « Publié » une release accessible dans le dépôt. Un correctif commun n’apparaît dans un APK installé ou une image téléphone qu’après leur mise à jour. -

Les paramètres de Gaia, écran Informations système

-

Paramètres de Gaia · capture de la version pour ordinateur.

+

+L’accueil de Gaia +Les paramètres de Gaia, écran Informations système +

+

Accueil et paramètres de Gaia · captures de la version pour ordinateur.

diff --git a/changelog.json b/changelog.json index 3626f5a6..0e89b424 100644 --- a/changelog.json +++ b/changelog.json @@ -1,8 +1,8 @@ { "schema": 1, "preamble": { - "fr": "# Journal des évolutions — Vulpes OS\n\n[English](CHANGELOG.en.md)\n\nUne base Gaia, trois plateformes : **PC**, **Android (APK)** et **Tundra / Sargo**.\nCe journal décrit les changements visibles et les étapes techniques importantes.\nLes rapports détaillés restent dans la documentation de chaque plateforme.\n\n**Statuts :** « En développement » concerne les sources locales, « APK construit »\nun paquet de test, et « Publié » une release accessible dans le dépôt. Un correctif\ncommun n’apparaît dans un APK installé ou une image téléphone qu’après leur mise à jour.\n\n

\"Les

\n

Paramètres de Gaia · capture de la version pour ordinateur.

", - "en": "# Changelog — Vulpes OS\n\n[Français](CHANGELOG.md)\n\nOne Gaia base, three platforms: **desktop**, **Android (APK)** and **Tundra / Sargo**.\nThis journal covers visible changes and major technical milestones.\n\n**Status:** “In development” refers to local sources, “APK built” to a test package,\nand “Published” to a public repository release. Shared source changes reach installed\nAPKs and phone images only after those packages are updated.\n\n

\"Gaia

\n

Gaia Settings · screenshot from the desktop version.

" + "fr": "# Journal des évolutions — Vulpes OS\n\n[English](CHANGELOG.en.md)\n\nUne base Gaia, trois plateformes : **PC**, **Android (APK)** et **Tundra / Sargo**.\nCe journal décrit les changements visibles et les étapes techniques importantes.\nLes rapports détaillés restent dans la documentation de chaque plateforme.\n\n**Statuts :** « En développement » concerne les sources locales, « APK construit »\nun paquet de test, et « Publié » une release accessible dans le dépôt. Un correctif\ncommun n’apparaît dans un APK installé ou une image téléphone qu’après leur mise à jour.\n\n

\n\"L’accueil\n\"Les\n

\n

Accueil et paramètres de Gaia · captures de la version pour ordinateur.

", + "en": "# Changelog — Vulpes OS\n\n[Français](CHANGELOG.md)\n\nOne Gaia base, three platforms: **desktop**, **Android (APK)** and **Tundra / Sargo**.\nThis journal covers visible changes and major technical milestones.\n\n**Status:** “In development” refers to local sources, “APK built” to a test package,\nand “Published” to a public repository release. Shared source changes reach installed\nAPKs and phone images only after those packages are updated.\n\n

\n\"The\n\"Gaia\n

\n

Gaia home screen and Settings · screenshots from the desktop version.

" }, "entries": [ { From 631de97a09e3c137a0fb819cef9df20288d9ae8f Mon Sep 17 00:00:00 2001 From: Alexandre NOEL <95a7002b-481b-4ef3-b578-f9c421550aa4@localhost> Date: Thu, 8 Oct 2026 22:26:01 +0200 Subject: [PATCH 07/12] Show both screenshots in READMEs; restore previous changelog illustration --- CHANGELOG.en.md | 7 ++----- CHANGELOG.md | 7 ++----- README.fr.md | 7 +++++-- README.md | 7 +++++-- changelog.json | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index 456a2a18..d88e6361 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -9,11 +9,8 @@ This journal covers visible changes and major technical milestones. and “Published” to a public repository release. Shared source changes reach installed APKs and phone images only after those packages are updated. -

-The Gaia home screen -Gaia Settings, system information screen -

-

Gaia home screen and Settings · screenshots from the desktop version.

+

Gaia Settings, system information screen

+

Gaia Settings · screenshot from the desktop version.

diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ba8c48e..c797ca7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,8 @@ Les rapports détaillés restent dans la documentation de chaque plateforme. un paquet de test, et « Publié » une release accessible dans le dépôt. Un correctif commun n’apparaît dans un APK installé ou une image téléphone qu’après leur mise à jour. -

-L’accueil de Gaia -Les paramètres de Gaia, écran Informations système -

-

Accueil et paramètres de Gaia · captures de la version pour ordinateur.

+

Les paramètres de Gaia, écran Informations système

+

Paramètres de Gaia · capture de la version pour ordinateur.

diff --git a/README.fr.md b/README.fr.md index 9362c19c..fe75f913 100644 --- a/README.fr.md +++ b/README.fr.md @@ -22,8 +22,11 @@ Vulpes OS reprend Firefox OS pour faire fonctionner **Gaia**, son interface et s 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. -

L’accueil Gaia de Vulpes OS sur ordinateur

-

L’accueil de Gaia dans la version pour ordinateur.

+

+L’accueil de Gaia +Les paramètres de Gaia, écran Informations système +

+

Accueil et paramètres de Gaia · captures de la version pour ordinateur.

## Essayer Vulpes diff --git a/README.md b/README.md index cf6afb8c..a3779e29 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,11 @@ Vulpes OS picks up where Firefox OS left off, adapting **Gaia**, its interface a 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. -

The Vulpes OS Gaia home screen running on desktop

-

The Gaia home screen in the desktop version.

+

+The Gaia home screen +Gaia Settings, system information screen +

+

Gaia home screen and Settings · screenshots from the desktop version.

## Try Vulpes diff --git a/changelog.json b/changelog.json index 0e89b424..3626f5a6 100644 --- a/changelog.json +++ b/changelog.json @@ -1,8 +1,8 @@ { "schema": 1, "preamble": { - "fr": "# Journal des évolutions — Vulpes OS\n\n[English](CHANGELOG.en.md)\n\nUne base Gaia, trois plateformes : **PC**, **Android (APK)** et **Tundra / Sargo**.\nCe journal décrit les changements visibles et les étapes techniques importantes.\nLes rapports détaillés restent dans la documentation de chaque plateforme.\n\n**Statuts :** « En développement » concerne les sources locales, « APK construit »\nun paquet de test, et « Publié » une release accessible dans le dépôt. Un correctif\ncommun n’apparaît dans un APK installé ou une image téléphone qu’après leur mise à jour.\n\n

\n\"L’accueil\n\"Les\n

\n

Accueil et paramètres de Gaia · captures de la version pour ordinateur.

", - "en": "# Changelog — Vulpes OS\n\n[Français](CHANGELOG.md)\n\nOne Gaia base, three platforms: **desktop**, **Android (APK)** and **Tundra / Sargo**.\nThis journal covers visible changes and major technical milestones.\n\n**Status:** “In development” refers to local sources, “APK built” to a test package,\nand “Published” to a public repository release. Shared source changes reach installed\nAPKs and phone images only after those packages are updated.\n\n

\n\"The\n\"Gaia\n

\n

Gaia home screen and Settings · screenshots from the desktop version.

" + "fr": "# Journal des évolutions — Vulpes OS\n\n[English](CHANGELOG.en.md)\n\nUne base Gaia, trois plateformes : **PC**, **Android (APK)** et **Tundra / Sargo**.\nCe journal décrit les changements visibles et les étapes techniques importantes.\nLes rapports détaillés restent dans la documentation de chaque plateforme.\n\n**Statuts :** « En développement » concerne les sources locales, « APK construit »\nun paquet de test, et « Publié » une release accessible dans le dépôt. Un correctif\ncommun n’apparaît dans un APK installé ou une image téléphone qu’après leur mise à jour.\n\n

\"Les

\n

Paramètres de Gaia · capture de la version pour ordinateur.

", + "en": "# Changelog — Vulpes OS\n\n[Français](CHANGELOG.md)\n\nOne Gaia base, three platforms: **desktop**, **Android (APK)** and **Tundra / Sargo**.\nThis journal covers visible changes and major technical milestones.\n\n**Status:** “In development” refers to local sources, “APK built” to a test package,\nand “Published” to a public repository release. Shared source changes reach installed\nAPKs and phone images only after those packages are updated.\n\n

\"Gaia

\n

Gaia Settings · screenshot from the desktop version.

" }, "entries": [ { From be2cdce8235b85873f830d2a15c506cc0396952e Mon Sep 17 00:00:00 2001 From: oversu <95a7002b-481b-4ef3-b578-f9c421550aa4@localhost> Date: Mon, 12 Oct 2026 23:56:09 +0200 Subject: [PATCH 08/12] Actualiser changelog.json --- changelog.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.json b/changelog.json index 3626f5a6..b2ab60b7 100644 --- a/changelog.json +++ b/changelog.json @@ -1,7 +1,7 @@ { "schema": 1, "preamble": { - "fr": "# Journal des évolutions — Vulpes OS\n\n[English](CHANGELOG.en.md)\n\nUne base Gaia, trois plateformes : **PC**, **Android (APK)** et **Tundra / Sargo**.\nCe journal décrit les changements visibles et les étapes techniques importantes.\nLes rapports détaillés restent dans la documentation de chaque plateforme.\n\n**Statuts :** « En développement » concerne les sources locales, « APK construit »\nun paquet de test, et « Publié » une release accessible dans le dépôt. Un correctif\ncommun n’apparaît dans un APK installé ou une image téléphone qu’après leur mise à jour.\n\n

\"Les

\n

Paramètres de Gaia · capture de la version pour ordinateur.

", + "fr": "# Journal des évolutions! — Vulpes OS\n\n[English](CHANGELOG.en.md)\n\nUne base Gaia, trois plateformes : **PC**, **Android (APK)** et **Tundra / Sargo**.\nCe journal décrit les changements visibles et les étapes techniques importantes.\nLes rapports détaillés restent dans la documentation de chaque plateforme.\n\n**Statuts :** « En développement » concerne les sources locales, « APK construit »\nun paquet de test, et « Publié » une release accessible dans le dépôt. Un correctif\ncommun n’apparaît dans un APK installé ou une image téléphone qu’après leur mise à jour.\n\n

\"Les

\n

Paramètres de Gaia · capture de la version pour ordinateur.

", "en": "# Changelog — Vulpes OS\n\n[Français](CHANGELOG.md)\n\nOne Gaia base, three platforms: **desktop**, **Android (APK)** and **Tundra / Sargo**.\nThis journal covers visible changes and major technical milestones.\n\n**Status:** “In development” refers to local sources, “APK built” to a test package,\nand “Published” to a public repository release. Shared source changes reach installed\nAPKs and phone images only after those packages are updated.\n\n

\"Gaia

\n

Gaia Settings · screenshot from the desktop version.

" }, "entries": [ From 1311bed5e96711b6cc9b516bc04f0ccf4880d850 Mon Sep 17 00:00:00 2001 From: OverSu Date: Tue, 13 Oct 2026 00:19:45 +0200 Subject: [PATCH 09/12] Release Preview 20 shared desktop, Android and Sargo updates Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source. Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately. --- CHANGELOG.en.md | 1069 ++++------ CHANGELOG.md | 1093 ++++------ README.fr.md | 8 +- README.md | 8 +- android/app/src/main/AndroidManifest.xml | 1 + .../vulpes_os/preview/AndroidServices.java | 21 + .../org/vulpes_os/preview/MainActivity.java | 11 +- android/extension/background.js | 40 +- android/extension/content.js | 55 + android/extension/manifest.json | 1 + android/tools/check-camera.py | 6 +- android/tools/pack-assets.py | 18 +- .../webapps/camera.gaiamobile.org/js/main.js | 4 + .../js/panels/app_permissions_list/panel.js | 4 +- .../js/panels/screen_lock_passcode/panel.js | 18 +- .../gaia_build_defer_index.js | 2 +- .../style/wallpaper.css | 3 + changelog.json | 776 ++++++- gaia/apps/camera/js/controllers/activity.js | 2 +- gaia/apps/camera/js/controllers/overlay.js | 6 +- gaia/apps/camera/js/controllers/storage.js | 2 +- gaia/apps/camera/js/controllers/viewfinder.js | 1 + gaia/apps/camera/js/lib/dcf.js | 2 +- gaia/apps/camera/js/lib/storage.js | 4 +- gaia/apps/camera/js/views/viewfinder.js | 3 + .../dialer/js/telephony_helper.js | 25 +- gaia/apps/gallery/js/MetadataParser.js | 39 +- gaia/apps/settings/js/modules/date_time.js | 11 +- .../settings/js/modules/dialog_manager.js | 54 +- .../settings/js/modules/dialog_service.js | 53 +- .../settings/js/modules/settings_service.js | 11 + .../app_permissions_list.js | 7 +- gaia/apps/settings/js/panels/help/support.js | 13 +- .../js/panels/screen_lock_passcode/panel.js | 45 +- .../panels/simcard_manager/simcard_manager.js | 4 +- .../settings/js/panels/wifi_status/panel.js | 3 +- gaia/apps/verticalhome/js/stores/item.js | 3 +- gaia/apps/video/js/video.js | 4 +- gaia/apps/wallpaper/style/wallpaper.css | 3 + gaia/shared/js/download/download_helper.js | 8 + host/Downloads.sys.mjs | 30 + host/Input.sys.mjs | 37 +- host/Messages.sys.mjs | 43 +- host/NativeCamera.sys.mjs | 5 +- host/Platform.sys.mjs | 90 +- host/Runtime.sys.mjs | 43 +- host/SettingsAudit.sys.mjs | 133 ++ host/SettingsFollowupAudit.sys.mjs | 56 + host/StorageStats.sys.mjs | 28 + host/Tundra.sys.mjs | 41 +- host/actors/VulpesChild.sys.mjs | 89 +- host/desktop.css | 50 + host/gaia-bluetooth.js | 126 ++ host/gaia-camera.js | 231 ++- host/gaia-compat.js | 51 +- host/gaia-input.js | 14 +- host/gaia-mobile-settings.js | 366 ++++ host/gaia-restoration.js | 175 +- host/gaia-select.js | 206 ++ host/gaia-services.js | 66 +- host/gaia-styles.js | 8 + host/gaia-theme.css | 105 + host/gaia-theme.js | 58 + host/gaia-wifi.js | 104 +- host/platform-ui.js | 17 +- host/shell.js | 17 +- overrides/camera.gaiamobile.org/js/lib/dcf.js | 101 + overrides/camera.gaiamobile.org/js/main.js | 1406 ++++++++----- .../js/MetadataParser.js | 713 +++++++ .../js/metadata_scripts.js | 1777 +++++++++++++++++ overrides/settings.gaiamobile.org/js/main.js | 435 +++- .../js/modules/date_time.js | 419 ++++ .../js/modules/dialog_service.js | 483 +++++ .../js/panels/display/panel.js | 49 +- .../js/panels/feedback_send/panel.js | 492 +++++ .../js/panels/firefox_accounts/panel.js | 508 +++++ .../js/panels/firefox_sync/panel.js | 505 +++++ .../js/panels/help/panel.js | 171 ++ .../js/panels/keyboard_add_layouts/panel.js | 490 +++++ .../js/panels/screen_lock/panel.js | 486 +++++ .../js/panels/simcard_manager/panel.js | 17 + .../js/panels/simpin/panel.js | 502 +++++ .../js/panels/usb_storage/panel.js | 487 +++++ .../js/panels/wifi/panel.js | 1 - .../wifi_select_certificate_file/panel.js | 485 +++++ .../js/panels/wifi_status/panel.js | 50 + .../resources/support.json | 1 + .../shared/js/download/download_helper.js | 693 +++++++ .../gaia_build_defer_index.js | 1177 +++++++++++ package-lock.json | 15 + package.json | 1 + release.json | 6 +- services/activities.mjs | 69 +- services/input-editor.mjs | 68 + services/input.mjs | 36 + services/mms.mjs | 29 + services/platform.mjs | 10 +- tests/ProbeChild.sys.mjs | 15 + tests/activities.test.mjs | 40 +- tests/android-input-bridge.test.mjs | 53 + tests/android-sms-handoff.test.mjs | 8 +- tests/bluetooth.test.mjs | 61 + tests/camera-lifecycle.test.mjs | 136 +- tests/input.test.mjs | 19 +- tests/messages.test.mjs | 10 + tests/mms.test.mjs | 34 + tests/platform.test.mjs | 37 + tests/settings-dialogs.test.mjs | 53 + tests/test_changelog.py | 115 ++ tests/test_publication.py | 4 +- tests/wifi-throttle.test.mjs | 5 + tools/amd-module-range.cjs | 14 + tools/audit-settings.py | 43 + tools/check-parity.py | 20 +- tools/export-changelog.py | 249 +++ tools/prepare-publication.py | 1 - tools/project.py | 7 + tools/refresh-camera-modules.py | 16 + tools/refresh-settings-panels.py | 36 +- tools/serve-gaia.py | 19 +- tools/sync-forgejo-changelog.py | 114 ++ tools/test-appearance.py | 59 + tools/test-call-screen.py | 18 +- tools/test-camera.py | 4 +- tools/test-composer-actions.py | 56 + tools/test-forgejo-feedback.py | 78 + tools/test-gaia-dialogs.py | 50 + tools/test-gaia-keyboard.py | 4 +- tools/test-hotspot-ui.py | 71 + tools/test-notification-swipe.py | 32 + tools/test-settings-followup.py | 25 + tools/test-settings-selector.py | 46 + tundra/droidian/export-runtime.py | 3 +- tundra/droidian/probe.sys.mjs | 228 ++- tundra/droidian/session.py | 1 + tundra/mms-codec/COPYING | 674 +++++++ tundra/mms-codec/go.mod | 10 + tundra/mms-codec/go.sum | 12 + tundra/mms-codec/main.go | 145 ++ tundra/mms-codec/main_test.go | 40 + tundra/runtime/audio-devices.py | 29 + tundra/runtime/bluetooth-control.py | 323 +++ tundra/runtime/camera-preview.py | 15 +- tundra/runtime/clock-service.conf | 3 + tundra/runtime/hal_policy.py | 6 +- tundra/runtime/hotspot-firewall.conf | 3 + tundra/runtime/mms-codec.py | 23 + tundra/runtime/mms-control.py | 326 +++ tundra/runtime/mobile-settings.py | 316 +++ tundra/runtime/prepare-bluetooth.py | 27 +- tundra/runtime/prepare-hardware.py | 40 +- tundra/runtime/prepare-native.py | 12 +- tundra/runtime/radio-control.py | 142 +- tundra/runtime/runtime-packages.py | 31 +- tundra/runtime/setup-hal.py | 4 + tundra/runtime/wifi-control.py | 147 +- tundra/tests/test_bluetooth_control.py | 22 + tundra/tests/test_hotspot.py | 69 + tundra/tests/test_mms_control.py | 69 + tundra/tests/test_mobile_settings.py | 115 ++ tundra/tests/test_network_clock.py | 31 + tundra/tests/test_radio_control.py | 104 + tundra/tests/test_ramdisk.py | 15 + tundra/tests/test_runtime_packages.py | 12 + tundra/tests/test_telephony_diagnostic.py | 65 + tundra/tests/test_wifi_control.py | 22 + tundra/tools/build-rescue-boot.py | 31 +- tundra/tools/diagnose-telephony.py | 116 ++ tundra/tools/refresh-boot-ui.py | 117 ++ tundra/tools/shared_ui.py | 51 + 170 files changed, 19733 insertions(+), 2530 deletions(-) create mode 100644 host/Downloads.sys.mjs create mode 100644 host/SettingsAudit.sys.mjs create mode 100644 host/SettingsFollowupAudit.sys.mjs create mode 100644 host/StorageStats.sys.mjs create mode 100644 host/gaia-bluetooth.js create mode 100644 host/gaia-mobile-settings.js create mode 100644 host/gaia-select.js create mode 100644 host/gaia-theme.css create mode 100644 host/gaia-theme.js create mode 100644 overrides/camera.gaiamobile.org/js/lib/dcf.js create mode 100644 overrides/gallery.gaiamobile.org/js/MetadataParser.js create mode 100644 overrides/gallery.gaiamobile.org/js/metadata_scripts.js create mode 100644 overrides/settings.gaiamobile.org/js/modules/date_time.js create mode 100644 overrides/settings.gaiamobile.org/js/modules/dialog_service.js create mode 100644 overrides/settings.gaiamobile.org/js/panels/feedback_send/panel.js create mode 100644 overrides/settings.gaiamobile.org/js/panels/firefox_accounts/panel.js create mode 100644 overrides/settings.gaiamobile.org/js/panels/firefox_sync/panel.js create mode 100644 overrides/settings.gaiamobile.org/js/panels/help/panel.js create mode 100644 overrides/settings.gaiamobile.org/js/panels/keyboard_add_layouts/panel.js create mode 100644 overrides/settings.gaiamobile.org/js/panels/screen_lock/panel.js create mode 100644 overrides/settings.gaiamobile.org/js/panels/simcard_manager/panel.js create mode 100644 overrides/settings.gaiamobile.org/js/panels/simpin/panel.js create mode 100644 overrides/settings.gaiamobile.org/js/panels/usb_storage/panel.js create mode 100644 overrides/settings.gaiamobile.org/js/panels/wifi_select_certificate_file/panel.js create mode 100644 overrides/settings.gaiamobile.org/js/panels/wifi_status/panel.js create mode 100644 overrides/settings.gaiamobile.org/resources/support.json create mode 100644 overrides/settings.gaiamobile.org/shared/js/download/download_helper.js create mode 100644 overrides/video.gaiamobile.org/gaia_build_defer_index.js create mode 100644 services/input-editor.mjs create mode 100644 services/input.mjs create mode 100644 services/mms.mjs create mode 100644 tests/android-input-bridge.test.mjs create mode 100644 tests/bluetooth.test.mjs create mode 100644 tests/mms.test.mjs create mode 100644 tests/settings-dialogs.test.mjs create mode 100644 tests/test_changelog.py create mode 100644 tools/amd-module-range.cjs create mode 100644 tools/audit-settings.py create mode 100644 tools/export-changelog.py create mode 100644 tools/refresh-camera-modules.py create mode 100644 tools/sync-forgejo-changelog.py create mode 100644 tools/test-appearance.py create mode 100644 tools/test-composer-actions.py create mode 100644 tools/test-forgejo-feedback.py create mode 100644 tools/test-gaia-dialogs.py create mode 100644 tools/test-hotspot-ui.py create mode 100644 tools/test-notification-swipe.py create mode 100644 tools/test-settings-followup.py create mode 100644 tools/test-settings-selector.py create mode 100644 tundra/mms-codec/COPYING create mode 100644 tundra/mms-codec/go.mod create mode 100644 tundra/mms-codec/go.sum create mode 100644 tundra/mms-codec/main.go create mode 100644 tundra/mms-codec/main_test.go create mode 100644 tundra/runtime/audio-devices.py create mode 100644 tundra/runtime/bluetooth-control.py create mode 100644 tundra/runtime/clock-service.conf create mode 100644 tundra/runtime/hotspot-firewall.conf create mode 100644 tundra/runtime/mms-codec.py create mode 100644 tundra/runtime/mms-control.py create mode 100644 tundra/runtime/mobile-settings.py create mode 100644 tundra/tests/test_bluetooth_control.py create mode 100644 tundra/tests/test_hotspot.py create mode 100644 tundra/tests/test_mms_control.py create mode 100644 tundra/tests/test_mobile_settings.py create mode 100644 tundra/tests/test_network_clock.py create mode 100644 tundra/tests/test_telephony_diagnostic.py create mode 100644 tundra/tools/diagnose-telephony.py create mode 100644 tundra/tools/refresh-boot-ui.py create mode 100644 tundra/tools/shared_ui.py diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index d88e6361..debb7487 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -2,805 +2,546 @@ [Français](CHANGELOG.md) -One Gaia base, three platforms: **desktop**, **Android (APK)** and **Tundra / Sargo**. -This journal covers visible changes and major technical milestones. +Desktop, Android (APK) and Tundra/Sargo. Status indicates development sources, a built APK or a published release. Source changes require an update to the installed package. -**Status:** “In development” refers to local sources, “APK built” to a test package, -and “Published” to a public repository release. Shared source changes reach installed -APKs and phone images only after those packages are updated. + -

Gaia Settings, system information screen

-

Gaia Settings · screenshot from the desktop version.

+## 2.7-preview.20 — APK built · 13 October 2026 + +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Preview 20 brings together shared fixes since APK Preview 17: Gaia settings, appearance, selectors, keyboard, contacts, Marketplace and media. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Camera: WebM recording with posters and Gallery/Video playback. Microphone permission requested; controls follow hardware capabilities. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Camera: more reliable sensor switching, stopping and resuming. Desktop and Android use their camera APIs; Sargo retains its HAL adapter. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo: Wi-Fi, Bluetooth, calls, SMS/MMS and network settings updates included in the sources. Unimplemented functions remain identified in their panels. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Changelog: simplified French/English text and badges retained; desktop/APK/Sargo filters independent of headings. Markdown, website and release notes generated from one JSON. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) APK signed with the previous Preview key; alignment and consistency of 53 shared files verified. Android runtime validation in progress. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Desktop/Sargo Gecko 156.0.1; Android GeckoView 156.0. Gaia 2.7.0. The APK delegates calls/SMS to Android; it does not replace Android telephony. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Experimental Preview: Mozilla account/Sync, remote location, Firefox OS add-ons and USB file transfer remain incomplete. Sargo: preview around 16 fps; recorded sound and call audio require separate qualification. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Pixel 3a: Preview 20 r62 RAM-booted, then flashed to boot_a. Matching readback hash; installed startup, eleven Gaia checks and ten native checks passed. Data retained, Wi-Fi connected and LTE registration observed. No new two-way audio call tested. + + + +## In development — 12 October 2026 — Video and clock settings + +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Camera: WebM VP8/Opus recording with microphone permission, poster and storage. Buffer limited to 32 MiB; recording stops before releasing the sensor. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Camera, Gallery and Video: handle WebM files and their posters. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Settings: original Gaia date/time pickers, Back from built-in keyboard settings and unclipped select labels. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Improve Vulpes: link to project issues. Unavailable services explained in their panels. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: simulated 1920 × 1080 video decoded; photo, gallery, camera resume and navigation checked. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo r61 in RAM: 480 × 640 video decoded, eight front/rear switches passed. Test media deleted; recorded audio not listened to. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo: preview measured around 16 fps versus 12 in the previous test. 30 fps is not reached yet. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: automatic NITZ time, persistent setting and guarded manual writes. Sync depends on carrier time; no NTP fallback, timezone selected manually. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Android: assets staged and Java compiled, without a new APK or Android video test at this stage. Mozilla account/Sync, remote location, Firefox OS add-ons and USB file transfer remain unimplemented. No flashing or publication. + + + +## In development — 12 October 2026 — Calls and Wi-Fi sharing + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Gaia dialogs: close after opening, complete interrupted transitions and allow retry after rejected validation. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: longer carrier-service timeout; slow settings separated from voice/SMS commands. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: call waiting, caller ID and four forwarding rules connected to oFono, with validation and readback. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: WPA2 or open Wi-Fi hotspot, NetworkManager configuration and compatible firewall. Private password, accessible only to Settings. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) r59 in RAM: 69 panels, dialogs and APNs checked on desktop/Pixel. Hotspot saved, started and stopped through Gaia; previous Wi-Fi restored. 44 Python tests and dialog regressions passed. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Internet forwarding to another device untested. Voicemail, call barring, PIN/PUK, Wi-Fi certificates and USB tethering remain open. Android assets synchronized; no new APK, flashing or publication. + + + +## In development — 12 October 2026 — Network settings + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Wi-Fi: actual IPv4 address and link speed; no “undefined” label when a measurement is missing. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: GSM/UMTS/LTE selection and editing of existing Internet/MMS APNs, with validation and readback. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) APNs: reject changes to active profiles, protect IMS and passwords, attempt rollback after failure and report its result. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo: bounded queue for slow settings. Call status read in 20 ms during a five-second settings wait. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) 38 Python tests, 69 desktop panels and APN/network workflows on RAM-booted Pixel checked. Android assets synchronized. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) APN creation, deletion and reset unavailable. Manual carrier selection and certificates still open. No new APK, flashing or publication. + + + +## In development — 12 October 2026 — Settings audit + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Settings: recover navigation after a controller or dialog fails; consistent module generation. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Accessibility filters and Vulpes support. Original panels retained with explanations for unavailable features. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Desktop: profile statistics and Gecko downloads. Android: private storage statistics. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: mobile data, roaming, clock and hidden Wi-Fi connected. Legacy network errors in Sound settings fixed. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) 97 views inventoried; 69 desktop/Pixel panels opened and seven native dialogs checked. Storage, data and timezone read back after writes on r51 in RAM. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Contextual views and unavailable services are not all qualified. Android Java compiled, without a new APK. No flashing, ticket closure or publication. + + + +## In development — 12 October 2026 — Settings navigation + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Dropdowns: Gaia selector with confirm/cancel; Light/Dark choice accessible. SIM manager adapted to one slot. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Unavailable panels: explanations and disabled controls instead of unresponsive links. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: saved Wi-Fi networks separated from slow scans; cache preserved during status reads. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: SIM information, SMS centre and delivery reports connected to the modem. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) r45 in RAM: eight appearance checks, five panels and actual SMS report setting changes passed, then setting restored. Desktop selector checked; APK assets staged. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Enterprise certificates and other carrier options still unavailable. No SMS sent, new APK, flashing or publication. + + + +## In development — 12 October 2026 — Shared fixes on Pixel 3a + +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo: r39 RAM boot with shared fixes and initial dark mode. r35 kernel and hardware configuration preserved. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Boot, viewport size, persistent settings, battery, call UI and Fira checked. Modem, audio, Wi-Fi and Bluetooth services active. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Seven Gaia appearance checks passed: propagated theme, readable headers, aligned labels and return to light mode. Pixel screenshot inspected. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Tool to update Gaia in a private boot image, with hashes, retained sources and checks for unchanged hardware components. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) No new call, SMS or pairing tested. Full theme and Android behavior still need qualification. No new APK, flashing or publication. + + + +## In development — 11 October 2026 — Appearance and shared keyboard + +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Appearance: Light, Dark or Follow system, shared between apps and retained across restarts. Wallpapers, photos and external websites preserved. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Keyboard: shared editing operations, selection and deletion, protection against late replies. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: nine appearance checks and Messages input gestures passed, without sending SMS. Visual review of Settings, Messages, Phone and Contacts. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Android: experimental Gaia keyboard option; Android keyboard remains the default, input mode restored on close. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android: briefly retry the same busy sensor, without retrying after permission denial or closing Camera. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Preview 19 built locally: signature, alignment, parity and bridge/camera tests passed. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Gaia keyboard, Android front camera and full theme still need qualification. Sources ready for Sargo, which remained on r35 at this stage. No upload, flashing or ticket closure. + + + +## In development — 11 October 2026 — Settings and homescreens + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Settings: ringtone management, PIN entry/deletion, lock disabling and third-party app permission list. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Home: first initialization of the second launcher and wallpaper previews. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Languages: refresh Vulpes labels after language changes; English fallback outside French. Dismiss camera failure panels and installation banners. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Android: open the Gaia tray by tapping the status bar. Preview 18 built, signed and compared with sources. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: nine Gaia workflows, five banner checks and twelve activity/camera tests passed. Sargo r35 Bluetooth confirmed by the user. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Compositor hot restart still unstable: files restored and return to r35. Fixes not yet qualified on Sargo at this stage; Android keyboard/theme open. No APK publication or flashing. + + + +## In development — 9 October 2026 — MMS and Bluetooth + +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Bluetooth: shared Gaia adapter, separate hardware commands and access restricted to authorized apps. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: power, discovery, explicit pairing, connection and removal from Gaia Bluetooth settings. Bluetooth audio modules included. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: Qualcomm network services and MMS transport connected. D-Bus disconnect no longer terminates the MMS daemon. Persistent state, retry without duplicates or uncertain automatic resends. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) r35 in RAM: image MMS visible and accepted by Orange MMSC; recipient delivery still unconfirmed. Five Bluetooth devices discovered, A2DP to desktop verified. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) 191 Tundra Python tests and 18 JavaScript tests passed. Smaller ramdisk and synchronized Android assets. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Headset call audio unqualified. No new APK, flashing or push at this stage. + + + +## In development — 8 October 2026 — Camera and MMS transport + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Camera: stop the stream before detaching video; await sensor closure, reset the viewfinder and recalculate size after rotation. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) MMS: preserve Blob attachments and keep messages pending until transport confirmation. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo: reused WebGL viewfinder without intermediate video capture; screen stays awake during preview. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop and Sargo: Camera/Gallery workflows and eight native switches checked. JPEG, focus and zoom working; rear ~16 fps, front ~25–27 fps in this test. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: persistent MMS queue, Nuntium codec and oFono transport. Text/photo UI tested with simulated transport. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Mobile data blocked carrier MMS at this stage. Android assets synchronized; no new APK, flashing or push. + + + +## 8 October 2026 — Call-log redial and error dialogs + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Gaia dialogs: visible text instead of a grey screen with a lone button. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Call log: redial through the same service as the keypad, without legacy Gecko APIs. Shared desktop/Android/Sargo file. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) MMS: clearly report unavailable sending, retain text and attachments, no false airplane-mode message. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: FR/EN dialogs and preserved composer. RAM-booted Sargo: 11 Gaia checks passed, redial tested with a simulated modem. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) No real call or audio validation at this stage. Android assets synchronized, no new APK published. + + + +## In development — 8 October 2026 — SMS and audio + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS: send in one tap while keeping the keyboard. Paperclip connected to the Gaia image, audio, video or contact menu. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) UI: visible volume indicator, lockscreen gestures and notification swiping fixed. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: keyboard, attachments, Gallery, call UI and volume checked; notification swiped without moving the page. No new APK published. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: PulseAudio access, Qualcomm relay, D-Bus protocol and calibration devices restored. Conflicting Android audio service removed. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: 11 Gaia checks, calibration and rear/front/rear previews passed. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Calls failed before ringing with a network error in this test. Two-way audio, Pixel gesture and MMS still unverified. No flashing or publication. + + + +## In development — 8 October 2026 — Telephony reliability + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: radio service survives client closure. No automatic new call or SMS after a lost response. Errors exclude phone numbers and message text. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Network/audio diagnostics without personal identifiers, and pinned carrier settings in the native image. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS: wait for the actual Gaia manager and accept uppercase oFono IDs. Conversation checks restored without resending. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: LTE restored after reboot, SMS confirmed sent by the modem, non-silent microphone and audio playback accepted. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Recipient delivery, two-way call audio and modem stability remain unqualified. Compositor hot restart unstable. No new APK, flashing or push. ## 8 October 2026 — Public source update -### Shared · Desktop / Android / Tundra - -- Bring recent camera, Contacts, SMS and Marketplace fixes, plus Sargo build and recovery tools, into the public sources. Update both READMEs with verified capabilities and limitations. -- Make the Android parity check create its report directory in a fresh checkout. Verify that native Wi-Fi configuration ships an empty connections directory. -- Keep tickets, roadmaps, internal documents, personal data and keys outside the repository. - -### Packages and qualification - -- The public APK remains Preview17; the 8 October fixes require a future build. No new native image or flash in this step. -- At the latest Sargo check, cellular registration is still searching; two-way call audio, Bluetooth pairing and profiles remain to be qualified. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Published Camera, Contacts, SMS and Marketplace fixes, plus Sargo build and recovery tools. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Updated French and English READMEs with verified features and limitations. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Create the report directory when checking Android asset parity in a fresh checkout. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Checked native Wi-Fi configuration: no bundled connection profiles. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Internal documents, personal data and keys excluded from the repository. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Public APK unchanged (Preview17); October 8 fixes not included. No new native image published. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo: modem still searching for a network; call audio, Bluetooth pairing and profiles await validation. ## 8 October 2026 — Camera and Contacts workflows · in development -### Shared · Gaia / Desktop / Android / Tundra - -- Serialize camera opening, await the previous sensor shutdown and release tracks and video resources after failures. Keep stable front/rear identities on desktop and Android. -- Preserve binary images across desktop/Tundra transport; explicitly encode contact photos and dates for Android's JSON bridge. -- Refresh contact details after editing. Restore Contacts → SMS, number picking and prefilled dialer activities, preserving nested requests and Gaia entry points. -- Reopen Gallery in picker mode when adding a contact photo, even when Gallery is already open for browsing. Selection, cropping and photo persistence verified on desktop. -- Add camera lifecycle, contact persistence, permissions, binary transport and activity routing tests. - -### Desktop - -- Verify the original Camera → JPEG capture → Gallery → photo opening → camera workflow. -- Verify Gaia contact creation, editing, detail refresh, accent-insensitive search and empty results, favourites, SMS recipient, number picker, prefilled keypad, cancelled deletion and confirmed deletion. The test sends no message and makes no call; its temporary contact is removed. - -### Tundra · Sargo - -- Verify on a RAM-booted Pixel 3a: autofocus, zoom, four front/rear switches with fresh frames, shutter capture, JPEG persistence, Gallery opening and sensor shutdown on Home. -- Check stored conversations, keyboard and notifications. Wi-Fi scan finds four networks. New SMS transmission/reception and call audio are still being qualified. -- Add SHA256-pinned BlueZ packages and required libraries. The controller starts, powers on and discovers; pairing through Gaia and Bluetooth profiles are not yet validated. -- Add native trusted certificates and verify HTTPS without disabling TLS. Test corrupted archives, file collisions and repeatable staging. - -- Include the corrected Contacts files in the boot build: contact creation, editing, SMS handoff, picking and dialer prefilling also pass using touch gestures on Sargo. - -### Android · APK - -- Publish the existing Preview17 APK and verify its download and SHA256. It predates the 8 October fixes. -- Fix recipient and body handoff from Gaia to the Android SMS app, including replies and a typed recipient that was not yet committed. Bridge tests pass; real Android validation is still required. -- Repackage corrected shared resources for the next build; no APK containing these new fixes has been published. - -**Status:** native qualification builds booted in RAM. No flashing, source push or private documentation publication in this step. Full phone qualification remains open. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Camera: corrected sequential opening, sensor cleanup and front/rear identification. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Contacts: corrected photo and date transport on desktop, Android and Tundra. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Contacts: restored refresh after editing, SMS handoff, number picking and dialer prefilling. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Gallery: open the contact photo picker even when the app is already running. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: include corrected Contacts files in the boot image. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android: pass recipients and text to the SMS app, including replies and unconfirmed typed numbers. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Tests for camera lifecycle, contact storage, permissions, binary transport and activities. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: verified JPEG capture, Gallery, contact photos, creation, editing, search, favourites and deletion. No calls or SMS sent. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo in RAM: verified autofocus, zoom, four camera switches, capture and Gallery; tested Contacts with touch input. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: checked stored conversations, keyboard and notifications; detected four Wi-Fi networks. New SMS and call audio await validation. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: BlueZ and dependencies pinned by SHA256. Power-on and discovery tested; pairing and profiles await validation. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: native certificate store. Tested HTTPS, corrupted archives, file collisions and repeated builds. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Published Preview17 with download and SHA256 checks. These fixes remain in source; Android SMS needs a real-device test. No Sargo flash. ## 6 October 2026 — Embedded Marketplace theme · in development -### Shared · Gaia / Desktop / Android / Tundra - -- Open the catalogue with `mode=app`, retained in navigation, search, filters, app details and language switches. -- Prepare a dedicated Vulpes web theme with local Fira fonts, compact navigation, touch-friendly controls, readable cards, collapsible filters and scrolling screenshots. Omit website headers in embedded mode while preserving the standard website layout. -- Keep confirmations and system messages in Gaia. The theme grants catalogue pages no additional privileges. -- Check 16 French/English pages, 2 standard pages and theme assets. Inspect mobile renders at 320 and 360 pixels. Desktop and Sargo launchers updated; shared sources will be included in the next Android packaging run. - -Website files prepared locally, not deployed to the public server. No push or new APK/ROM publication. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Marketplace: embedded theme with local Fira fonts, compact navigation, touch controls, collapsible filters and scrolling screenshots. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Preserve `mode=app` across navigation, search, filters and language changes. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Hide website headers in embedded mode. Gaia handles confirmations; the catalogue receives no extra privileges. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Checked 16 French/English pages, 2 standard pages and mobile layouts at 320 and 360 pixels. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Theme ready locally, not deployed. Desktop and Sargo launchers updated; Android integration pending the next APK. ## 6 October 2026 — Gaia banners · in development -### Shared · Gaia / Desktop / Android / Tundra - -- Dismiss the “Application installed” banner after its normal timeout even if the animation-end event does not reach the Gaia component host. Keep its appearance and tap-to-dismiss behavior. -- Cancel old timers when a new message replaces the previous one, preventing premature dismissal. -- Check display, expiry, replacement and click dismissal on desktop; verify actual removal after five seconds on the Pixel. Fix deployed to the Sargo RAM trial and available in shared sources for the next APK. - -No flash or publication in this step. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Automatically dismiss the “Application installed” banner even without an animation-end event. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Cancel old timers when a new banner replaces the previous one. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: checked display, expiry, replacement and click dismissal. Sargo: verified dismissal after five seconds in RAM. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Fix available in shared sources for the next APK. No flash or release. ## 6 October 2026 — Wi-Fi and Settings panels · in development -### Shared · Gaia / Desktop / Android / Tundra - -- Complete Settings panel navigation even when Gecko does not emit an animation-end event. Preserve Gaia transitions and prevent duplicate initialization. -- Updated shared sources and the desktop server; included in the next Android packaging run. No new APK published in this step. - -### Tundra · Sargo - -- Fix Wi-Fi signal events: include the associated network and do not emit connection information while disconnected. The incomplete event prevented Gaia from displaying scan results. -- Check the driver, native service, Gecko bridge and corrected panel on the Pixel: four access points detected, grouped into two networks, with no scan error. -- Wi-Fi connection confirmed by the user; a real HTTPS request from Gecko to example.org passed (HTTP 200, certificate verification enabled). Fixes applied to the RAM trial; no flash or Git push. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Settings: open panels even without an animation-end event, without duplicate initialization. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: corrected Wi-Fi events to restore network results and report disconnection properly. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: detected four access points grouped into two networks. Connection confirmed and HTTPS request passed with certificate verification. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Shared sources, desktop server and Sargo RAM trial updated. Fixes not included in the public APK. ## 6 October 2026 — Marketplace · in development -### Desktop / Tundra · Sargo - -- Keep the catalogue below the status bar and inside its owning application. Hide its web view on Home and in the task switcher; remove it when the app closes. -- Prevent a cancelled navigation or a previous error page from hiding a retry. -- Show a local error message with a retry action when the catalogue is unavailable, instead of leaving its error page over Gaia. -- Desktop navigation, network failure, retry, Home and closing from the task switcher checked. Seven direct checks pass on Sargo in RAM without a debugger window, using a local page and a forced-unlock precondition; the remote catalogue requires an Internet connection on the phone. - -### Android · APK - -- Rebuild the local Preview17 APK with the shared Marketplace error message. Signature, alignment and asset parity checked; no new Android device regression run in this step. - -Nothing published in this step. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Desktop/Sargo: keep Marketplace below the status bar, hide it on Home and remove it when closed. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Cancelled navigation or an old error page no longer hides a retry. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Local error message with a Retry button when the catalogue is unavailable. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: verified navigation, network errors, retry, Home and closing. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo in RAM: seven checks passed with a local page and forced unlock. Remote catalogue not validated without Internet access. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Rebuilt local Preview17 APK; checked signature, alignment and assets. No new Android device test or release. ## 2.7-preview.17 — APK built · 5 October 2026 -### Shared · Gaia / Desktop - -- Restore Marketplace on Home with its original icon and the Vulpes catalogue. Select French for fr languages and English otherwise. -- Turn catalogue downloads into confirmed installations: validate ZIP archives, add the application to Home and support launch and removal. -- Store installed applications separately from Gaia sources, on distinct origins and without system privileges inherited from archived manifests. -- Validate the real desktop workflow with 2048: catalogue, confirmation, installation, launch, persistence and removal. Check damaged archives, invalid paths, limits, cancellation and single-use confirmations. - -### Android · APK - -- Build and sign Preview17 with the same Marketplace and installation service. Use the existing GeckoView browser for the catalogue and private Android storage for packages. -- Verify the Install click, confirmation, Home icon and 2048 launch without access to the Vulpes system bridge on an Android 16/API36 emulator. Also verify persistence after force-stop/relaunch and removal. - -### Tundra · Sargo - -- Prepare a local boot image containing Marketplace, the installer and the added-application server. RAM boot performed on the Pixel on 6 October: Home and Marketplace resources verified, without flashing. Nine of ten system checks pass; the overall Gaia check records a viewport change during unlocking. Package installation on Sargo still needs qualification. - -**Limitations:** initial support for packaged web applications. Old privileged APIs, hosted apps and discontinued services are not restored automatically. Nothing published to Forgejo or the website. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Marketplace on Home with its original icon and the Vulpes catalogue in French or English. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Confirmed ZIP app installation, launch from Home and removal. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Installed apps isolated from Gaia sources and system privileges. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: full 2048 workflow plus archive, path, limit, cancellation and single-use confirmation checks. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Preview17 APK built and signed with Marketplace, GeckoView and private package storage. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Android 16/API 36 emulator: 2048 installation, launch, isolation, persistence after app restart and removal. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo in RAM on October 6: Home and Marketplace assets verified, 9 of 10 system checks passed. Viewport changes on unlock; package installation awaits validation. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Initial support for packaged web apps. Legacy privileged APIs and hosted apps not restored. No release at this stage. ## 2.7-preview.16 — APK built · 5 October 2026 -### Shared · Gaia / Desktop - -- Restore Gaia wallpaper and sound pickers, return navigation to Settings and cancellation through Home. Share persisted image and sound transport between desktop and Android. -- Propagate language changes to Home without granting general settings read access. Check French/English labels and persistence. -- Restore horizontal page snapping and hide development apps from Home without deleting their sources. -- Use the supplied Vulpes logo in the splash and adapt the original sound-list components to the current engine. -- Verify two consecutive wallpaper changes, sound selection and restored Gaia panels on desktop. - -### Android · APK - -- Build and sign local Preview16; verify shared payload and version. Nothing published in this step. -- Validate Vulpes as the HOME launcher, restore Gaia locking after sleep and Android Back navigation from the lockscreen camera. -- Hide Android system bars in immersive mode while retaining temporary swipe access. -- Android 16/API36 emulator checks: locking, camera/Back, French/English, Home without test apps, wallpaper and sound. Wallpaper retained after force-stop/relaunch with no repeated changes over 15 seconds. Other Android devices not qualified in this step. - -### Tundra · Sargo - -- Include shared fixes and app policy in a new test boot image. The previous boot reapplied an old file snapshot, undoing hot updates. -- RAM boot on Pixel passed: six language/Home/picker checks and ten system checks. Image/sound picker results verified without replacing personal selections. Installed boot remains Preview15; no partition flashed in this step. - -**Limitations:** third-party FFAA installation remains open. The site depends on old mozApps APIs and a nomodule script; an installer with application and permission isolation still needs implementation. Selecting a Gaia sound does not change the global Android ringtone; actual call audio was not qualified here. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Restored Gaia wallpaper and sound pickers, return to Settings and cancellation through Home. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Apply language changes to Home and preserve them after restart. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Restore horizontal page snapping; hide development apps on Home. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Vulpes startup logo and sound list adapted to the current engine. Media transport shared by desktop and Android. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android: Gaia locking after sleep and Back from the lockscreen camera. HOME launcher mode validated. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Android: hide system bars in immersive mode, with swipe access. Preview16 APK built and signed. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop and Android 16/API 36 emulator: checked wallpapers, sounds and panels; tested language, locking and wallpaper persistence on Android. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: updated the boot image to stop restoring outdated files. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo in RAM: 6 UI and 10 system checks passed. Installed build remains Preview15, without flashing. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) APK not published. FFAA installation and call audio await validation; Gaia sound selection does not change the Android ringtone. ## In development — 4 October 2026 — Standalone Sargo installation -### Tundra · Sargo - -- Build a fresh ext4 volume directly for userdata, including the system, writable data and vendor/modem hardware images. Startup no longer looks for files in a Droidian installation; installation requires neither an Android session nor ADB. -- Python Fastboot installer checks the Sargo model, unlock state, partition sizes and checksums, then replaces userdata, dtbo_a, vbmeta_a and boot_a. The bootloader stays unlocked and device-specific calibration is retained. -- Full offline userdata backup, complete comparison against the phone and recovery archive verification before flashing. Backups remain private; an actual restoration was not performed in this step. -- Two successful boots from internal storage: eleven Gaia and ten native checks per boot, verified boot-image readback and retained settings. Slot A is marked successful only after validating Gaia startup in the current boot. -- Generic variant without an SSH server or maintenance keys: actual boot_a flash and boot passed. The development variant was then restored on the Pixel for further work. Generic package images retain their original fresh state. -- Final checks on the standalone development boot passed for Camera (including lens switches), the Gaia keyboard and notifications. -- Local Fastboot package assembled with checksums and a qualification report. 44 targeted tests pass. It is not published yet: provenance and redistribution of some hardware components still need final review. -- Limits: qualified on one Pixel 3a, not every firmware combination; an 8 GiB volume including 1 GiB of writable data, without automatic expansion. Hardware functionality remains experimental. - -### Desktop / Android · APK - -- No interface changes or new binary in this step. Gaia remains shared by all three platforms. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: standalone ext4 userdata volume with system, data and vendor/modem components. Installation independent of Droidian and ADB. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Fastboot installer with model, unlock, size and checksum checks. Replaces userdata, dtbo_a, vbmeta_a and boot_a; retains calibration. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Full userdata backup and recovery archive verified before flashing. Actual restoration not tested. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Two installed boots passed: 11 Gaia and 10 native checks per boot, retained settings and matching boot readback. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Variant without SSH or maintenance keys booted after flashing. Development variant reinstalled; camera, keyboard and notifications checked. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Local Fastboot package assembled; 44 targeted tests passed. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Package unpublished: hardware component provenance and redistribution pending. Tested on one Pixel 3a; 8 GiB volume with 1 GiB writable, without automatic expansion. ## In development — 3 October 2026 — Per-device Sargo installation -### Tundra · Sargo - -- Read-only collection of six boot partitions and existing storage, bound to the phone identity and slot A. Backups remain private. -- Portable device workspace: RAM builds, boot checks, installation and recovery export can use it without the development Pixel’s shared inventory. Workspace-bound builds reject implicit fallback to that inventory. -- Checked SSH transfers verify identity, storage and checksums and create files without replacing existing data. The transfer tool never formats, flashes or reboots. -- Build and transfer tested on the Pixel. First RAM boot passed eleven Gaia and ten native checks; keyboard and notifications passed. Camera testing encountered an autofocus timeout, retained in the report. -- Second boot of the same image retained the Gaia setting and passed eleven Gaia and ten native checks; camera, keyboard and notifications passed. The first autofocus timeout remains documented, not declared fixed. -- Verify return to the installed build: Gaia starts and the boot partition is unchanged. A repeated transfer correctly refuses to replace existing data. -- Refine HAL provenance: identify the installed package and download recipe and retrieve historical UBports build recipes. Exact correspondence with the old Android artifact and resolved source manifest remains unverified. -- Still limited to the qualified kernel and compatible ext4/LVM storage. This does not yet provide direct installation from stock Android. - -### Desktop / Android · APK - -- No interface changes or new build in this step. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: portable device workspace for builds, installation and recovery, independent of the development Pixel inventory. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Read-only collection of six boot partitions and storage, bound to the phone identity. Private backups. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) SSH transfer with identity, storage and checksum checks, without overwriting, formatting or flashing. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Two RAM boots: 11 Gaia and 10 native checks passed each; setting retained. Autofocus timed out on the first trial; camera worked on the second. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Return to the installed build verified; boot unchanged and data overwrite rejected. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Autofocus timeout not considered fixed. Exact HAL provenance still pending; qualified kernel and compatible ext4/LVM storage required, without direct installation from stock Android. ## In development — 3 October 2026 — Sargo preparation and recovery tools -### Tundra · Sargo - -- Add local package preparation: verify manifest and archive checksums, bound decompression, check ext4 and create a fresh data image for each installation. Existing directories are never overwritten. -- Add a private recovery kit that can be exported outside the checkout. It retains the original boot_a backup and a standalone tool that checks model, serial number, slot and checksums before explicit restoration. It is not a user-data backup. -- Existing restoration no longer requires the diagnostic image to remain available. Backup validation is retained. -- System builds now require an explicit ARM64 hardware reference and reject display-only references or missing required services. -- Prepare the real system archive and verify the recovery kit locally. No new flash or public ROM release; automated installation on another Sargo still needs completion. - -### Shared · Desktop / Android / Tundra - -- Retain the shared JSON changelog in future source publications; internal Markdown working documents remain excluded. - -### Desktop / Android · APK - -- No functional changes or new build in this step. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: local package preparation with checksum checks, bounded extraction, ext4 validation and fresh data creation without overwriting. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Private recovery kit with boot_a backup and model, serial, slot and checksum checks. User data not included. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Restoration works without the diagnostic image. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) System builds require a complete ARM64 hardware reference. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) System archive prepared and recovery kit checked locally. Installation on another Sargo pending; no flash or ROM release. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) JSON changelog included in future source releases; working documents excluded. ## In development — 3 October 2026 — Sargo modem recovery and fresh installation -### Tundra · Sargo - -- Rebuild the system image with the font-cache fix and creation of oFono’s private state directory on first boot. -- Limit recovery to the modem subsystem: a controlled restart went offline, recovered and registered on the network in about 22 seconds without rebooting the phone. This does not fix the firmware’s IMS watchdog root cause. -- Check Gaia startup, hardware rendering, Fira fonts, both camera sensors, focus/zoom, keyboard and visual notifications with a fresh profile. Wi-Fi scanning still works after the modem restart. -- Two successful boots retained the same Gaia setting in a separate data image. Camera, keyboard and notifications rechecked after the second boot. Installed boot partition and existing data retained. -- No new native image published: the public installation/recovery package remains unfinished. - -### Desktop / Android · APK - -- No shared interface changes or new APK build in this step. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: corrected font cache and first-boot creation of oFono’s private directory in the system image. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Modem recovery without rebooting the phone. Network registration took about 22 seconds in a controlled trial. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Fresh profile: checked Gaia, hardware rendering, Fira, both cameras, focus, zoom, keyboard and notifications. Wi-Fi scan worked after modem restart. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Two boots retained settings in a separate data image. Existing installation preserved. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) IMS watchdog root cause unresolved. Public installation and recovery package still incomplete; no image released. ## October 1, 2026 — Sargo distribution preparation — in development -### Tundra · Sargo - -- Rebuild Preview 15 system image with empty profiles, internal working documents excluded, license notices retained and an inventory of 493 runtime packages. -- Filesystem and content checks completed; these checks do not qualify hardware boot. -- Read-only storage inventory and mount-table generation from that inventory instead of hardcoded development-phone dimensions. Limited to a single ext4 extent on Sargo userdata; no formatting added. -- Clean image transferred and verified on the charged Pixel. Temporary RAM boot tests left boot_a unchanged; automatic return to installed v59 and Gaia startup checked. -- SIM detection, LTE registration and Wi-Fi scanning checked on this image; these checks do not establish end-to-end calling, SMS or mobile data. -- Fix first-boot font cache ownership by running fc-cache as the session user. Gaia startup, hardware rendering, Fira, both camera sensors, keyboard and visual notifications checked in RAM. Qualification of the rebuilt image remains incomplete. -- Intermittent reboot traced to modem IMS firmware; targeted recovery policy applied and another boot with LTE, camera, keyboard and notifications passed. Recovery after a new IMS crash has not been exercised. The public image remains non-flashable and unpublished. - -### Desktop / APK - -- No functional changes or new APK build in this step. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo: rebuilt Preview15 image with empty profiles, licenses and an inventory of 493 packages. Working documents excluded. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Mount table generated from the storage inventory. Support limited to a single ext4 extent on userdata. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) First-boot font cache permissions: run fc-cache as the session user. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Filesystem, content and transfer checks passed. RAM trials left boot_a unchanged; return to v59 verified. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) SIM, LTE, Wi-Fi scan, Gaia, hardware rendering, Fira, cameras, keyboard and notifications checked in RAM. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Targeted recovery after tracing intermittent reboots to IMS firmware. Second boot passed; recovery from another IMS crash not tested. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Image qualification incomplete; end-to-end calls, SMS and mobile data unvalidated. Image unpublished and non-flashable. ## 2.7-preview.15 — 30 September 2026 — published +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) First publication of desktop, Android and Tundra sources, with French/English READMEs, licenses and setup scripts. Private files excluded. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS: corrected bubble dates and sent/failed states. Improved camera stream cleanup and replacement. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Desktop: stabilized navigation checks, adjusted cold-start timeout and clean shutdown after Gaia times out. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop with Gecko 156.0.1: 42 service tests, startup, navigation, settings, gestures, history, isolation and persistence checked. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Preview15 APK signed with the previous key and GeckoView 156.0; asset parity verified. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Android 16 emulator: validated update, Camera/Gallery, camera resume, ten background returns and renderer recovery. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo: sources included, private v59 retained. No generic ROM; boot depends on storage layout and preinstalled images. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo: preview around 16 fps; flash, video, rotation and deep suspend incomplete. Call audio and the dialog after SMS sending need investigation. + [Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15) -### Shared · Gaia / Desktop / Android / Tundra - -- First shared source publication, including English/French READMEs, licenses and setup scripts. Internal documents, roadmap, profiles, keys and private images excluded from the public repository. -- Include the latest SMS fixes (bubble dates, sent/failed states) and camera lifecycle fixes. The two 30 September entries detail the hardware changes. - -### Desktop PC - -- Published checkout tested with Gecko 156.0.1 and a fresh profile: Gaia startup, navigation, settings, gestures, history and page isolation. 42 service tests pass; service persistence after restart checked. -- Stabilize navigation checks during normal document replacement and allow more time for cold startup. Close the host cleanly even if Gaia exceeds its preparation timeout. - -### Android · APK - -- Preview 15 signed with the previous Preview key, GeckoView 156.0; shared resource parity checked. Installed as an update on an Android 16 emulator. -- Photo/Gallery capture, image display and sensor resume validated. Ten background/resume cycles and recovery after renderer termination validated. This does not qualify every Android phone. - -### Tundra · Sargo - -- Runtime sources included; private v59 remains the build qualified on the development phone. No generic flashable ROM published: boot still depends on inventoried storage, preinstalled images and private maintenance access. -- Limits remain: approximately 16 fps camera preview, incomplete flash/video, rotation and deep suspend; call audio awaiting qualification and a large dialog after SMS sending still under investigation. - ## In development — SMS and camera feedback · 30 September 2026 -### Shared · Gaia / Desktop / Tundra - -- Messages: serialize bubble dates as numbers. Fix empty conversations caused by date formatting errors. Composer flow and sent/received bubbles verified on desktop with a simulated modem. -- Camera: make release idempotent; detach videos, await sensor release and dispose of the WebGL context before switching cameras. Identify each session so an old stream cannot stop its replacement. Repeated rear/front switches produce fresh frames; Home return and sensor shutdown passed on two RAM boots. - -### Tundra / Sargo - -- Vibration: use the DRV2624 brightness control; the legacy activate command did not physically stop the motor. The user confirmed stopping; single-pulse, 200/200/200 ms pattern and service-stop checks all returned to zero. Add a stop command when the service starts or exits. -- The user confirms focus, zoom and SMS transport both ways on v54. Five existing conversations render on the Pixel, with message bubbles and no date errors. -- v59 installed in boot_a after two successful RAM boots; installed startup and readback verified, slot A marked successful. Desktop sources and runtime synchronized. The large dialog after a real send still needs identification; camera smoothness remains separate work. - -### Android · APK - -- Shared Gaia sources updated; no APK rebuilt or published. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS: convert dates to fix empty conversations. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Camera: corrected stream and sensor cleanup; an old session can no longer close its replacement. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: physical vibration shutdown through DRV2624, with a safeguard at service startup and exit. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: SMS composition and display checked with a simulated modem. Sargo: camera switching and shutdown tested over two RAM boots. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v54: user confirmed focus, zoom and two-way SMS. Five conversations displayed; vibration shutdown confirmed. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo v59 installed on boot_a after two RAM boots; startup, readback and slot A verified. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Post-SMS dialog and camera smoothness still pending. Shared sources updated, without a new APK. ## In development — Messages and Camera · 30 September 2026 -### Desktop / Tundra - -- Messages now retain the message and conversation after a modem rejection and dispatch Gaia sending, failed and sent events. -- Distinguish the oFono queue from confirmed transmission; persist terminal states. Modem confirmation is not a recipient delivery receipt. -- Desktop checks cover failed messages visible in conversations, touch capture, Gallery opening and camera release/resume. - -### Tundra / Sargo - -- Connect touch focus areas, digital zoom and sensor JPEG capture to the original Gaia Camera. Preview stays at 640 × 480; directly measured JPEG sizes are 4032 × 3024 (12.19 MP) rear and 3264 × 2448 (7.99 MP) front. The front camera has fixed focus. -- Keep HAL logs out of the binary protocol, bound replies and resume preview after capture. A focus failure must not close the session. -- Reject SMS submission explicitly when unregistered; retain sent/failed states after the oFono object disappears. Carrier messages triggered Gaia notifications on v49 after LTE registration recovered. -- Qualification: two v54 RAM boots and the installed boot_a startup passed; image installed on the Pixel and slot marked successful after readback. Persistence, Gaia startup and Camera checks passed. Network registration remains unstable: SMS tests stopped before transmission, with no end-to-end delivery confirmed. - -### Android · APK - -- No APK rebuilt or published in this pass. The available Preview 14 is unchanged. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Desktop/Tundra: retain SMS after modem rejection and pass sending, failed and sent states to Gaia. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: persist SMS states after the oFono object disappears; reject sending when the modem is unregistered. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: touch focus, digital zoom and sensor JPEG capture in Gaia Camera. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Measured photos: 4032 × 3024 rear, 3264 × 2448 front. Preview 640 × 480; fixed front focus. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Native camera: separate binary data from HAL logs, bound responses and resume preview after capture or focus errors. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: checked visible send failures and Camera/Gallery workflows. Sargo v54: validated two RAM boots, installation, persistence and camera. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Unstable network; no end-to-end SMS delivery confirmed in this pass. Modem confirmation is not a delivery receipt. Public APK unchanged (Preview14). ## 2.7-preview.14 — APK published · 30 September 2026 +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) System information: version alignment and Fira fonts in Shadow DOM components. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Desktop/Tundra: portrait key sizing, cursor placement in the active field and stale input replies. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Desktop/Tundra: Gaia keyboard bridge and numeric keypad for SMS recipients. Android keeps its system keyboard. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Lockscreen: resize without a keyboard window. Honour silent notifications, including after restoration. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: persistent tray and lockscreen notifications, opening the owning application. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: cancellable vibrations capped at ten seconds; private persistent Wi-Fi profiles, automatic reconnection and network removal. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: store incoming SMS before acknowledgement, remove duplicates and create the state directory. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: earpiece/speaker, microphone mute and call-volume controls. Routing, mute and volume tested outside a call. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Installer: reject flashing diagnostic images with a debugger. Sargo v48 installed after two RAM boots; persistence, readback and slot A verified. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: Calendar and keyboard checked. Sargo: keyboard, notifications and Wi-Fi reconnection checked; notification sound and real telephony await validation. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Preview14 APK signed and published. Startup, versions and fonts checked on an Android 16 emulator. No Sargo image released. + [Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.14) -The Preview 14 APK is published. Desktop is up to date and the private Sargo v48 build is installed and tested; this release does not include a public phone image. - -### Shared · PC / APK / Gaia - -- About: remove the extra indentation in version rows above “Last updated”; alignment checked on desktop and Android. -- Fonts: normalize text font names inside Shadow DOM stylesheets, including styles added later; keep icon fonts intact. -- Calendar: month view opens without JavaScript errors in the desktop check. - -### PC / Tundra - -- Keyboard: use the screen orientation instead of the short keyboard frame. Keys measure about 60 CSS pixels on Pixel; rotation tested on desktop at DPR 3. -- SMS: restore the caret to the focused field when Gecko retains the recipient selection; preserve valid selections during editing. -- Gaia notifications: respect silent, no-sound and no-vibration flags, including restored notifications. - -- Gaia keyboard: native input bridge, numeric SMS recipient layout, corrected selection events and protection against stale field replies. Recipient and message typing tested on desktop and Pixel; carrier delivery still requires qualification. -- Lockscreen: tolerate the absence of a keyboard window during resize. -- Native notifications: persistent application-owned entries and launch through Gaia's declared Phone entry point. Tray and lockscreen display/removal verified on Sargo; audible sound still needs qualification. - -### Tundra / Sargo — v48 build installed - -- Vibration: Sargo actuator bridge, patterns limited to ten seconds, non-blocking sequencing and cancellation. Driver activation and stop measured. - -- Wi-Fi: private persistent profiles, autoconnect and forget operation; storage and failed-update rollback tests. Connection and reconnection after a RAM reboot verified on Sargo; private profile files (0600). -- Incoming SMS: persist before acknowledgement and deduplicate retries. Services now create their state directory at startup, fixing a mount failure reproduced on Sargo. -- Call audio: earpiece/speaker, microphone mute and call volume controls added; earpiece/speaker switching, mute and volume verified without placing a call. Two-way conversation still unverified. -- Local-debugger images cannot be flashed by the installer. v48/preview.14 written to boot_a after two RAM boots and persistence checks; installed boot verified, image readback matches and slot A marked successful. v43–v48 do not enable a debugger. - -### Android · APK - -- Preview.14 APK built, signed and published on Forgejo. Startup, version information, alignment and component font selection checked on an Android 16 emulator. Android retains its system keyboard. - ## 2.7-preview.13 — application workflows · 27 September 2026 -### Shared · Gaia / Desktop - -- Calendar: successfully scheduling a reminder no longer rejects event saving; scheduling failures reach the caller. -- Reminders: ignore deleted events and do not mark denied notifications as delivered. Native Tundra notification support remains incomplete. -- Notifications: accept empty raw text without requesting an “undefined” translation. -- Gallery: positive thumbnail size even when starting hidden; cancel preview updates after leaving the editor. -- SMS: apply correspondent, read state, date range and delivery filters; cover conversation reading and deletion. -- Deeper review: WebM playback, seeking and deletion; black-and-white photo editing and saved copy; event creation and deletion. Retain original Gaia interfaces. - -- Isolated checkout qualification passes startup, desktop tests, persistence and Android packaging without rewriting sources. - -### Android · APK - -- Signed preview.13 APK built; Camera/Gallery, Calendar, ten background/resume cycles and renderer recovery validated on an Android 16 emulator. - -### Tundra · Sargo - -- Build v40/preview.13 installed after two RAM boots: Gaia, camera, Wi-Fi, brightness and controls checked; matching boot_a readback and successful slot marking. Remaining limits in `docs/APPLICATION-REVIEW.md`. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Calendar: save events with alarms, report scheduling errors and skip reminders for deleted events. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Notifications: accept empty text; do not mark rejected notifications as delivered. Native Tundra support still incomplete. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Gallery: thumbnail sizing at hidden startup and cancellation of updates after closing the editor. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS: apply correspondent, read-state, date and delivery filters. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: checked WebM playback, photo editing and copying, calendar, conversation reading and deletion. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Isolated checkout: startup, desktop tests, persistence and Android packaging passed. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Preview13 APK built and signed. Camera/Gallery, Calendar, ten background returns and renderer recovery validated on emulated Android 16. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v40 installed after two RAM boots; Gaia, camera, Wi-Fi, brightness, controls and boot_a readback verified. ## 2.7-preview.12 — application review · 27 September 2026 -### Shared · Gaia / Desktop - -- Restore original Gaia 1×–2.25× raster variants for 1,047 CSS image references. -- Camera switch becomes available after device discovery; unavailable flash stays - visible and disabled; pinching no longer zooms the whole camera application. -- Reserve Home gestures for long press and suppress its context menu; retain Gaia's task manager. -- Relay Gaia touch gestures using unprivileged DOM events, without granting trusted - user activation to external websites. -- Bundle the supplied bilingual, local browser welcome page and route links and - searches into the actual browser. -- Welcome page follows the Gaia language live: `fr`/`fr-*` select French; - all other languages select English. -- SMS: numeric identifiers, migration of older records, Date values, sent events, - multi-recipient request shape and serialized storage writes. -- Calendar: restore the current day's accessibility label in 37 locales. -- Review 14 apps; 26 media/clock/UI checks, contact and SMS checks, plus real Web - navigation and isolation. Scope and limitations: `docs/APPLICATION-REVIEW.md`. - -### Android · APK - -- Rebuild `2.7-preview.12` with the same shared UI and local welcome page; check - payload parity. Android camera capture continues to use getUserMedia. - -- Tundra hardware features are not automatically ported to Android: each platform - retains its adapter and reports its actual capabilities. - -### Tundra · Sargo - -- Convert NV21 using a shader and transfer binary frames instead of base64: - approximately 23 fps measured in RAM, versus 8 fps in the intermediate trial. -- Wait for camera worker exit before opening the other sensor. -- Manual brightness reaches its target in about 200 ms in the hardware test; - keep screen sleep/wake fades. -- Connect Gaia Wi-Fi power settings to actual radio state; support WPA-PSK on - mixed WPA2/WPA3 networks. -- Scan, connection and HTTPS over wlan0 validated in RAM; connection profiles are temporary. -- Wi-Fi profiles are still temporary. Flash, video, light/orientation sensors, - Bluetooth, data counters and advanced call controls remain incomplete. -- v38 installed: two RAM boots, installed startup, `boot_a` readback, other boot - partitions preserved and slot A marked successful. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Gaia icons: restored 1× to 2.25× variants across 1,047 CSS references. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Camera: enable switching after sensor detection and prevent pinch zoom on the UI. Unavailable flash disabled. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Home: restore long press and gesture forwarding without additional privileges for websites. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Browser: local bilingual welcome page following Gaia’s language without restarting. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS: corrected identifiers, migration, dates, sending events, multi-recipient requests and concurrent writes. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Calendar: restored the current-day accessibility label in 37 languages. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Reviewed 14 apps; 26 Gallery/Music/Clock/UI checks, Contacts/SMS tests and web isolation checks. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Preview12 APK rebuilt with shared assets; parity verified. Hardware features handled separately on Android and Tundra. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo: shader-based NV21 preview and binary transport, about 23 fps measured versus 8 in the intermediate trial. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: wait for camera worker shutdown before switching sensors; manual brightness reaches its target in about 200 ms. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: Wi-Fi power control from Gaia and WPA-PSK connections to mixed WPA2/WPA3 networks. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: scan, connection and HTTPS validated in RAM. v38 installed after two RAM boots, with readback and slot A checks. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo: temporary Wi-Fi profiles; flash, video, sensors, Bluetooth, data counters and advanced call controls incomplete. ## In development — Sargo feedback and interface · 27 September 2026 -### Shared · Gaia / Desktop - -- Shutdown animation: three blue shades replace the orange rings; original Gaia - dimensions, sequence and timing are preserved. - -- Phone: prevent document scrolling on tab changes, keeping Recents and Contacts - headers in place. Use Gaia’s original call-log artwork at multiple densities up to 2.25×. -- Select controls: remove the old engine offset that clipped Fira descenders. - Video: keep the introductory text within the viewport. -- Brightness: save a number from input/change events; normalize legacy text values - before Gaia animates the transition. Coalesce stale hardware brightness steps - while preserving the order of sleep/wake commands. -- Home: extend wallpaper beneath the transparent gradient, reserve navigation space - inside applications, and place Home on the right in landscape. -- Browser: editable local page at `overrides/search.gaiamobile.org/home/index.html`, - retaining Gaia integration and private mode. -- Wi-Fi: show a scan failure and retry action instead of endless retries. Network - discovery and results in the original Gaia panel now verified on Sargo in RAM. -- Consolidated tracking: `docs/CHANTIER.md` includes earlier and new feedback and limitations. - -### Android · APK - -- Build and sign `2.7-preview.11` with shared Gaia; payload parity checked. - Place Home on the right in landscape and fix its wallpaper background. - Check rotation and non-overlapping app/navigation bounds on Android 16 emulator. - -### Tundra · Sargo - -- Camera: hardware preview in Gaia, JPEG capture and rear/front switching verified - on Sargo in RAM; sensors close on returning Home. Capture is currently limited - to 640 × 480 (480 × 640 portrait); video and flash remain unavailable. - Desktop and APK keep getUserMedia and share the same Gaia interface. - -- v33 (`2.7-preview.11`) installed after two successful RAM boots. Installed boot, `boot_a` readback, - data preservation and slot A success finalization verified. -- Backlight measured from Gaia: 35% → 89/255, 80% → 204/255. Wi-Fi results visible - in the original panel. Blue shutdown rings included. -- Call audio: support PulseAudio 14 text output and prevent loading audio modules twice. - Voice-call profile, earpiece selection and media-profile restoration verified on Sargo. - An actual two-way conversation remains unvalidated. -- Still open: audible two-way calls, full-resolution camera capture/video and sensor-driven rotation. - See the tracking document. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Gaia shutdown animation recoloured blue. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Phone: keep headers fixed when changing tabs and use call-log icons up to 2.25× density. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Clipped dropdown text and overflowing Video welcome text. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Brightness: numeric values and coalesced stale commands, preserving sleep/wake order. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Home: wallpaper under the gradient and right-side button in landscape, without covering apps. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Browser: editable local welcome page, retaining private mode. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Wi-Fi: error message and Retry button instead of endless retries. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Preview11 APK built and signed; rotation and Home button placement checked on emulated Android 16. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: preview, JPEG and camera switching validated in RAM. Capture limited to 640 × 480; flash and video unavailable. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v33 installed after two RAM boots; startup, data, readback and slot A verified. Brightness and Wi-Fi results checked in Gaia. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: PulseAudio 14 compatibility and duplicate audio module loading. Call/media profiles and earpiece tested. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo: call audio, full-resolution capture, video and automatic rotation await validation. ## In development — native Wi-Fi stability · 27 September 2026 -### Shared · Gaia / Desktop - -- Fix the Wi-Fi loop with no delay: scan every 20 seconds as intended by Gaia. - Coalesce concurrent scans. Add burst, failure and timer regression tests. -- Shared development version: `2.7-preview.9`, Gaia `2.7.0`; actual Gecko versions - remain unchanged. - -### Android · APK - -- Build `2.7-preview.9` to retain shared Gaia parity. Android still manages its - own Wi-Fi; this change does not claim to add a hardware driver to the APK. - -### Tundra · Sargo - -- Identify the v21/v22 failure: unbounded Python Wi-Fi scan clients exhausted - memory, causing the lowmemorykiller to terminate Gecko. The phoc SIGSEGV occurs - afterwards during display teardown. -- Allow one hardware scan at a time, briefly cache results and errors, invalidate - after connect/disconnect. Application isolation and memory protection remain enabled. -- Qualify v25 through two RAM boots, then install and verify it on the Pixel: - 11 Gaia and 10 system checks pass, with no memory-pressure process kills. - Check the Gaia lockscreen, Fira, battery, date and Wi-Fi panel; exercise - sleep/wake and volume through injected input events. -- Verify `boot_a` by readback, preserve the other boot partitions and user data, - and mark slot A successful. Native camera support and full call validation - remain unfinished. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Wi-Fi: restore the 20-second scan interval and coalesce concurrent scans. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: one hardware scan at a time, with a short cache cleared on connect/disconnect. Fixes memory exhaustion in v21/v22. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Wi-Fi burst, failure and timer tests. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Shared Preview9 version, Gaia 2.7.0; new APK with the same sources. Android still manages its own Wi-Fi. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v25: two RAM boots then installation, 11 Gaia and 10 system checks passed, without low-memory process kills. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Checked locking, fonts, battery, date and Wi-Fi; tested sleep/wake and volume with injected events. Data, readback and slot A verified. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Native camera and full call validation still pending. ## In development — Gaia restoration · 27 September 2026 -### Shared · Gaia / Desktop - -- Restore Gaia panels and remove temporary windows/floating controls: Settings, - Usage, FM and contact/media import. Features without an API remain visible with - disabled controls, without invented hardware values. -- Fix legacy style scoping: ordinary `div` selectors no longer become component - roots. Restore lockscreen icons, retain Fira and use scalable Gaia glyphs for - Phone tabs. -- Reserve 44 CSS pixels for Home so it cannot cover Answer/Hang up. The date uses - its original component; battery updates reach Settings and its labels no longer - display `{{ level }}`. -- Re-enable the original Gaia lockscreen; temporary splash uses the supplied logo, - stable dimensions and a gentle animation. Localized name: Navigateur / Browser. - Credits appear in Legal Information; original license notices remain. -- All 26 application checks pass (Gallery, Music, Clock, task management). Further - desktop checks cover panels, imports, battery, date and navigation; simulated - calls, history, sleep/wake, Power menu and volume. See `docs/GAIA-RESTORATION.md`. - -### Android · APK - -- Build and install `2.7-preview.8` on the Android 16 emulator with shared Gaia; read battery state from Android, - add Android Settings at the bottom and optional Android Home mode. The Android - application catalogue in Gaia still needs integration. -- Android checks: ten background/resume cycles and renderer termination recovery; - shared payload parity verified. No physical Android device test in this pass. -- Imports use Gaia components; the SMS button opens Android confirmation. - The internal lockscreen does not replace Android security. - -### Tundra · Sargo - -- Build v21/v22 candidates with all `overrides/` fixes in the boot payload; connect Wi-Fi - scan/association to the Gaia panel through NetworkManager. -- RAM trials blocked: the display service exits with code 139 before qualification. - No new flash. Restore installed v20 and verify 11 Gaia and 9 system checks with - preserved data; restore the native profile’s previous unlocked state. Candidate - Wi-Fi, native camera, full SIM/Bluetooth APIs and CPU suspend remain unqualified. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Restored Gaia Settings, Usage, FM and import panels. Temporary interfaces removed; unavailable features disabled. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Corrected legacy component styles, lockscreen icons and Phone glyphs. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Reserve 44 pixels for Home; restore date and battery without `{{ level }}` placeholders. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Gaia lockscreen re-enabled, splash stabilized, Browser name localized and credits added. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: 26 app checks passed, plus panels, imports, battery, navigation and simulated calls. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Preview8 APK installed on emulated Android 16: Android battery, settings access and optional HOME mode. Android app catalogue still incomplete. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Emulated Android: ten background returns, renderer recovery and asset parity checked. Gaia locking remains separate from Android security. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo v21/v22: Wi-Fi scan and association connected to Gaia through NetworkManager. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: RAM trials interrupted by display-service failure (code 139). Return to installed v20 verified, without a new flash. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Candidate Wi-Fi, camera, SIM/Bluetooth APIs and CPU suspend unvalidated. ## In development — 27 September 2026 -### Shared · Gaia / Desktop - -- Replace the temporary call overlay with Gaia’s original **Callscreen**: - incoming/outgoing calls, answer, hang up and automatic close. A compatibility - adapter supplies `mozTelephony` events to the original modules. Unsupported - audio/conference controls remain disabled. -- Persist completed calls in the host and import them into Gaia’s original call - log, with acknowledgement and duplicate detection after interruption. -- Connect real battery state to the original status icon; unknown is not empty. - Handle a missing/malformed Facebook contacts index without blocking local - number lookup or call history. -- Compare rendered font faces: home already used Fira on both platforms, but - desktop system text partly used Noto. Main text rules now select Gaia’s - bundled Fira face while preserving icon fonts. -- Desktop Gecko test with a simulated modem covers incoming/outgoing UI, - answer/hang up, history, screen sleep/wake, power menu, volume and battery. - No APK rebuilt in this step. - -- Earlier v13 step (superseded above): system-owned call screen with dialing, ringing, connected state and Hang up. - Controls remain available if the service temporarily loses call status. -- Tested in desktop Gecko with a simulated modem, including hang-up failure and - retry. Desktop sources synchronized; no APK rebuilt. - -### Tundra · Sargo - -- Private v20 installed and read back in `boot_a` after two RAM boots and one - installed boot; settings retained and slot marked successful. -- Read only Sargo’s physical Power/Volume keys and forward - their edges to Gaia HardwareButtons. Short press sleeps/wakes the screen; - long press opens Gaia’s menu. The restricted native broker controls backlight, - volume, shutdown and reboot. -- Serialized brightness and power changes: concurrent requests no longer - leave the backlight disabled while waking the screen. -- Native volume: expose the session PulseAudio socket read-only to the service; - otherwise Gaia’s indicator changed while the audio output remained unchanged. -- Screen sleep does not suspend the kernel yet. Real incoming calls, conversation - audio and advanced call audio controls remain to be qualified. - -- Earlier step: private v13 installed in `boot_a` after two RAM boots; installed boot, - readback and the A/B success flag verified. -- oFono call state reporting and Answer/Hang up commands restricted to modem call paths. -- Successful void D-Bus responses no longer cause a false invalid-response error. -- A short real outgoing call reached ringing and was automatically hung up. - Conversation audio and incoming calls remain unqualified. -- Camera diagnostics no longer mistake a timeout for successful initialization. - Libhybris detects two sensors, but the preview probe receives no frames. - Native Gaia camera access remains unavailable. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Gaia Callscreen for incoming/outgoing calls, answer and hangup. Replaces the temporary v13 screen. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Persistent call log with acknowledgement and duplicate detection. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Distinguish unknown battery state from empty battery; tolerate missing or malformed Contacts indexes. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Render system text with Fira Sans while preserving icon fonts. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop with simulated modem: calls, failed hangup and retry, history, sleep/wake, Power menu, volume and battery. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: Power/Volume buttons connected to Gaia, with native screen, volume, shutdown and reboot controls. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: serialize brightness and power changes to prevent a black screen on wake; fix PulseAudio access for volume. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: successful hangup with an empty D-Bus reply no longer produces a false error. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v13 then v20 installed after RAM trials; startup, readback, settings and slot validation checked. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Real outgoing call reached ringing and was hung up. Incoming calls and conversation audio unvalidated. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Camera diagnostics: a timeout no longer counts as successful initialization. Two sensors detected, without received frames. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Native camera unavailable, CPU suspend and advanced audio controls incomplete. No new APK. ## In development — 26 September 2026 -### Shared · Gaia - -- Fonts: use the archived simulator’s Fira Sans OpenType files for both Gaia - pages and Tundra system text. Fix native sans-serif fallback and add Fontconfig - validation; icon fonts remain separate. -- Settings: remove the “Tundra settings” and “Test a notification” diagnostic - buttons. Release information and the technical details remain available. -- Hardware panel: pass platform capabilities into the panel. An undefined variable - used to open a Gecko alert instead of displaying battery information. - -### Desktop - -- Local adapter synchronized with the shared sources. Gecko remains at 156.0.1. - -### Android - -- Hardware panel fix is in the shared sources; no new APK has been built. - -### Tundra / Sargo - -- Explicit A/B boot-success finalization after installation: backed-up and checked - GPT metadata, verified Qualcomm utility, Fastboot confirmation. -- Modem rejections distinguished from transport failures; errors shown inside Gaia. - The unavailable native camera no longer suggests an ineffective permission change. -- Rendered-font and radio diagnostics without automatic calls or messages. Fira Sans - confirmed on the home screen and status bar. -- Installed the v9 font build into `boot_a` on the inventoried Sargo. Written - image verified by readback; the other five backed-up boot partitions are unchanged. -- Two boots from device storage pass 10 Gaia and 9 system checks each and retain - settings. A PC is no longer required to boot. Droidian fallback files retained; - actual restoration has not been tested. -- Private persistent candidate: separate 2 GiB ext4 data image, retained Gaia - settings/profiles and system identities, no ten-minute reboot watchdog. -- Two consecutive boots of the same v8 image: 9 Gaia and 9 system checks pass on - each boot. Separate diagnostic: 15 checks using identical shared UI files. -- Boot-a-only installation and original-boot restoration tools prepared and tested - locally. No actual flash performed; remaining hardware limits are documented. -- Normal startup no longer opens Settings/Clock or scrolls the home screen. These - actions now belong to the explicit `--diagnostics` mode. -- Enter fullscreen before loading Gaia and hide native window decorations. On the - tested Pixel: 360 × 740 CSS pixels at scale 3, stable dimensions and an unchanged - home screen throughout five post-startup samples. -- v13 RAM trial passes eight startup checks and nine system checks. Reports now - distinguish startup observation from interactive qualification. -- Separate v14 diagnostic: thirteen interactive checks pass, including the hardware - panel; Wi-Fi panel and scanning verified with the same UI files as normal v13. -- Interactive diagnostic images are rejected as the normal launch target. The - earlier v13 selection is retained in the launcher history. -- Modem registered on UMTS during the trial; audio playback completed and microphone - samples were measured without retaining a recording. In-call audio, camera and - suspend remain unqualified. -- Ramdisk dependency discovery now runs `readelf` in the C locale, preserving the - ARM64 dynamic loader regardless of the build machine's language. - -All six A/B boot/DTBO/vbmeta partitions are unchanged. This remains a private RAM -trial, **not a flashable ROM**. See [Tundra status](tundra/docs/STATUS.md). +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Use historical Fira Sans fonts in Gaia and Tundra; correct sans-serif fallback and Fontconfig checks. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Removed diagnostic buttons from Settings. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Hardware panel: pass platform capabilities correctly to display the battery. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: distinguish modem and transport errors; correct the unavailable-camera message. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: persistent data on a 2 GiB ext4 image and A/B success validation after installation. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v8: two boots with persistence, 9 Gaia and 9 system checks per boot. Installation and recovery tools tested locally. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v9 installed on boot_a: two boots with 10 Gaia and 9 system checks each. Matching readback, five other partitions preserved; actual restoration untested. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Normal startup without automatic diagnostic gestures; fullscreen before Gaia, stable 360 × 740 CSS-pixel layout. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Separate `--diagnostics` mode; diagnostic images excluded from normal launch. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: v13 tested in RAM (8 startup, 9 system checks), v14 diagnostics (13 interactive checks), including Wi-Fi. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) UMTS registration, audio playback and microphone signal checked. Call audio, camera and suspend unvalidated. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Ramdisk builds independent of the desktop language by running readelf in the C locale. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Private trials, without a public ROM. Desktop sources synchronized; shared Android fix available, without a new APK. ## In development — 25 September 2026 -### Shared · Gaia - -- **Navigation:** a circular Home button, a black-to-transparent gradient over the wallpaper, - and a solid blue bar inside apps. Pressing Home now changes the ring instead of leaving - a blue rectangle over the home screen. -- **Scrolling:** the gradient disappears at the bottom of the home panel and returns when - scrolling up. Switching between the Apps and Pages panels is supported. -- **Typography:** bundled Fira Sans fonts, with multiple weights and italics, instead of - relying on the host system’s installed fonts. -- **Device information:** separate product, Gaia, actual engine, platform, model and adapter - versions. The panel includes support for Tundra and hardware adaptation fields; - connecting these to the native platform is still pending. -- **Icons:** loading resumes after an initial zero-width measurement, across desktop, - Android and Tundra. This fix was previously limited to Android. -- **Maintenance:** this changelog brings together the three platforms’ progress. - -### Desktop · Locally tested - -- Tested an icon initially measured at zero width, then made visible. -- Tested Home / Settings / Home navigation, a transparent bar at the bottom, the gradient - at the top and permission checks in an isolated profile. -- Tested the information panel in French and English, without duplicate legacy rows. -- The engine remains Gecko **156.0.1**; these UI changes do not rebuild Gecko. - -### Android · Sources adapted, Java compiled - -- The GeckoView / Android bridge implements the same navigation states: an overlay on Home, - with space reserved for the bar in apps and the browser. -- Navigation state survives Android activity recreation. -- Java compilation passed. **No new APK delivered for these changes**; - visual behaviour still needs testing on Android. - -### Tundra / Sargo · Private RAM trials - -- Network build v6 is the launcher reference after 13 Gaia and 9 system checks; - automatic return and unchanged hashes of all six boot partitions verified. -- Separate Tundra backend reports Pixel 3a, Sargo adaptation and native version, - without claiming an unimplemented camera or telephony stack. -- Real battery and charging state with a bilingual Gaia hardware panel. - The network trial passed 13 Gaia checks, including this panel. -- Real Wi-Fi tested: scan, connection, IPv4, DNS and HTTPS in Gecko. Added - `pd-mapper`, disabled uninitialized IPA offload and premature camera probing; - 9 system checks pass without kernel Oops. Gaia network selection is still - pending; trial credentials remain in RAM. -- Service configuration can travel in the small ramdisk, avoiding another 3 GiB - transfer per boot adjustment. Qualcomm trials use RAM copies of modem NV data. - -- A new ext4 system image contains the ARM64 runtime, reference HALs, Phoc, Gecko and Gaia. - The runtime is no longer loaded from the Droidian working directory, nor the HAL - from its previous `/var/lib/lxc/android/` location. -- The second Pixel 3a trial passed **7 Gaia checks and 8 system checks**, with Tundra PID 1, - its HAL services, compositor and Gaia. -- A physical display capture confirms home icons, loaded Fira Sans, the circular button - and gradient. Gecko **156.0.1 ARM64**, with hardware WebRender. -- Small UI changes travel in the trial ramdisk, avoiding another 3 GiB system transfer - for each CSS or JavaScript adjustment. -- Build sources are saved alongside new images and verified by hashes, so qualified - images remain testable after the checkout changes. -- The latest trial (`sargo-system-boot-20260925-v5`) passes **10 Gaia checks and 8 system - checks**, including icons, fonts, pressed Home, bottom-of-page behaviour and app return. - Physical capture reviewed; local launcher reference updated. No APK published or phone flashed. - -**Limitations:** the installed filesystem still carries the system image; writes are lost -at shutdown; the trial returns to the retained system after ten minutes. No flashing or -Droidian replacement. A warm compositor restart failed during one trial; cold boot works. -Networking, modem, audio, camera and suspend still need integration or qualification. -This is not a public installable ROM or a validated daily-use phone. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Round Home button, transparent wallpaper gradient and blue bar inside apps. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Hide the gradient at the bottom and restore it when scrolling; remove the blue rectangle when pressing Home. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Bundled Fira Sans weights and italics; separate product, Gaia, engine and platform versions. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Resume icon loading after an initial zero-size measurement on all three platforms. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: icons, navigation, permissions and French/English information panel checked with Gecko 156.0.1. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android: retain navigation state after activity recreation. Java compilation passed; visuals need retesting, no new APK delivered. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Tundra: ext4 image with ARM64 runtime, HAL, Phoc, Gecko and Gaia; small fixes bundled in the ramdisk. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: dedicated backend, Pixel 3a information and hardware battery readings. Build sources archived with checksums. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: hardware WebRender, icons and fonts checked on a physical display capture. v5: 10 Gaia and 8 system checks passed. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: add pd-mapper, disable uninitialized IPA and premature camera probing. Copy modem NV data to RAM for trials. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v6: Wi-Fi scan, connection, IPv4, DNS and HTTPS verified; 13 Gaia and 9 system checks passed. Gaia network selection still pending. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) RAM trials without flashing, automatic return after ten minutes and changes lost at shutdown. Cold boot passed; warm compositor restart failed. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) No public ROM; modem, audio, camera and suspend still need integration or validation. ## 2.7-preview.7 — APK built, local reference -### Shared / Desktop / Android - -- Fixed internal camera allocation permission on desktop and recovery after the first - CAMERA permission prompt on Android. -- Photo distinguishes permission denial, a missing sensor and an opening failure. -- Shared product version **2.7-preview.7**, Gaia **2.7.0**, with separate engine versions. -- Desktop Gecko **156.0.1**, Android GeckoView **156.0**: the real patch difference is - reported rather than relabelling either engine. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Desktop camera permission and recovery after the first Android permission prompt. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Separate messages for denied permission, missing sensors and opening failures. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Separate product and engine versions: Vulpes Preview7, Gaia 2.7.0, desktop Gecko 156.0.1 and Android GeckoView 156.0. ## 2.7-preview.6 — APK built -### Shared / Desktop / Android - -- Ported the original Gaia Camera app: webcam preview and JPEG capture on desktop, - or capture through GeckoView on Android. -- Photos saved in Gaia Gallery; media imports use the host system’s file picker. -- Unsupported controls disabled, including flash and video; camera switching depends - on available sensors. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Gaia Camera on desktop and Android: preview and JPEG capture through webcam or GeckoView. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Save photos in Gallery and import through the system picker. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Flash and video disabled; camera switching depends on available sensors. ## 2.7-preview.5 — APK built -### Shared - -- Centralised versions in `release.json`, separating product, Gaia and engine versions. -- Shared native integration UI between desktop and Android; packaged common files - checked inside the APK before distribution. - -### Desktop - -- Integration with available Linux settings panels, media and vCard imports, - desktop notifications and a test webcam capture. - -### Android - -- Same Gaia interface and native commands as desktop, adapted to Android capabilities. -- Foreground return and recovery after rendering-process termination tested. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Versions centralized in `release.json`; product, Gaia and engine separated. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Shared desktop/Android native interface and APK asset parity checks. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Desktop: Linux settings, media/vCard imports, notifications and test webcam capture. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Android: foreground return and recovery after renderer termination. ## 2.7-156.0.2 — Historical APK built (revision 4) -This historical name mixed product revision and engine version; it does not mean -that the APK used Gecko 156.0.2. Its Android engine was Gecko 156.0. - -### Android - -- Activity and session recovery to reduce white screens when returning to Vulpes. -- Access to Android settings, volume and window brightness. -- Media and contact imports, native notifications with permissions. -- Handoff to Android’s dialer, SMS and clock apps; no silent sending or replacement - of the system Phone and SMS apps. -- Capture initially delegated to Android’s camera app, before the Gaia Camera port. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android: activity and session recovery to reduce blank screens. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Access to Android settings, volume and window brightness. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Media and contact imports; native notifications with permissions. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Handoff to Android Phone, SMS, Clock and Camera. No silent sending or replacement of default apps. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Historical numbering: Android engine Gecko 156.0, despite the 2.7-156.0.2 name. ## 2.7-156.0.1 — APK published (revision 3) -- **Vulpes OS - Preview** name and project icon. -- Wallpaper flickering fix included. -- Historical numbering retained; it does not identify the Android engine’s patch version. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) “Vulpes OS - Preview” name and project icon. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Wallpaper flickering corrected. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Historical numbering, separate from the Android engine version. [Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1) @@ -808,7 +549,7 @@ that the APK used Gecko 156.0.2. Its Android engine was Gecko 156.0. ## 2.7-preview.2 — APK published -- Fixed wallpaper flickering in the Android Preview. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Wallpaper flickering in the Android Preview. [Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.2) @@ -816,8 +557,8 @@ that the APK used Gecko 156.0.2. Its Android engine was Gecko 156.0. ## 2.7-preview.1 — APK published -- First Android Preview bundling Gaia and GeckoView. -- Archived version; known wallpaper flickering issue. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) First Android Preview with Gaia and GeckoView. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Archived version; known wallpaper flickering issue. [Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.1) @@ -825,26 +566,12 @@ that the APK used Gecko 156.0.2. Its Android engine was Gecko 156.0. ## Earlier milestones — technical retrospective -These milestones are not newly dated public releases. - -### Gaia / Desktop - -- Vulpes assembled from historical Gaia: branding, wallpaper, translations and - work on system information. -- Experimental Gecko 45 → 52 port, followed by separation of Gaia, services and - the engine for the current Gecko host. -- Reimplementation of required APIs: settings, application registry, local contacts, - messages, alarms, storage and inter-app communication. -- Adaptation of old UI components, simulated touch, browser and applications; - testing of archived Firefox OS apps. -- Gecko update tools with checks, candidate engines and explicit rollback. - They detect incompatibilities without automatically repairing them. -- Gaia launch and updates without rebuilding the whole engine. - -### Tundra / Sargo - -- Pixel 3a inventory, boot partition backups and USB diagnostic trials. -- QEMU trials, then ARM64 Gecko inside Droidian and under a dedicated compositor. -- Separate root, selected HAL services and Tundra PID 1; temporary graphical - Fastboot trials with rescue SSH and automatic return. -- Reference kernel **4.9-124-google-sargo**, **Android 9 / API 28** adaptation. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Historical Gaia base with Vulpes branding, wallpaper, translations and system information. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Experimental Gecko 45 to 52 port, followed by separation of Gaia, services and engine. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) APIs for settings, apps, contacts, messages, alarms, storage and inter-app communication. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Adapt Gaia components, touch gestures, browser and apps to recent engines. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Gecko updates with checks and rollback; Gaia updates without rebuilding the engine. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Archived Firefox OS apps, QEMU, then ARM64 Gecko inside Droidian and under a dedicated compositor. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: inventory, boot backups, separate root, HAL services and Tundra PID 1. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Temporary graphical Fastboot trials with SSH recovery and automatic return. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Kernel 4.9-124-google-sargo, Android 9/API 28 adaptation. Historical milestones, without newly dated releases. diff --git a/CHANGELOG.md b/CHANGELOG.md index c797ca7c..ac32180c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,828 +1,547 @@ -# Journal des évolutions — Vulpes OS +# Journal des modifications — Vulpes OS [English](CHANGELOG.en.md) -Une base Gaia, trois plateformes : **PC**, **Android (APK)** et **Tundra / Sargo**. -Ce journal décrit les changements visibles et les étapes techniques importantes. -Les rapports détaillés restent dans la documentation de chaque plateforme. +PC, Android (APK) et Tundra/Sargo. Les statuts indiquent des sources en développement, un APK construit ou une version publiée. Les changements des sources nécessitent une mise à jour du paquet installé. -**Statuts :** « En développement » concerne les sources locales, « APK construit » -un paquet de test, et « Publié » une release accessible dans le dépôt. Un correctif -commun n’apparaît dans un APK installé ou une image téléphone qu’après leur mise à jour. + -

Les paramètres de Gaia, écran Informations système

-

Paramètres de Gaia · capture de la version pour ordinateur.

+## 2.7-preview.20 — APK construit · 13 octobre 2026 + +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Preview 20 réunit les corrections communes depuis l’APK Preview 17 : paramètres Gaia, apparence, sélecteurs, clavier, contacts, Marketplace et médias. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Photo : enregistrement WebM avec miniature et lecture dans Galerie/Vidéo. Autorisation micro demandée ; commandes adaptées aux capacités du matériel. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Caméra : changement de capteur, arrêt et reprise fiabilisés. PC et Android utilisent leurs API caméra ; Sargo conserve son adaptateur HAL. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo : mises à jour Wi-Fi, Bluetooth, appels, SMS/MMS et réglages réseau incluses dans les sources. Les fonctions non raccordées restent indiquées dans les panneaux. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Changelog : reprise des textes simplifiés FR/EN, badges conservés, filtres PC/APK/Sargo indépendants des titres. Markdown, site et notes de release issus du même JSON. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) APK signé avec la clé des Preview précédentes ; alignement et cohérence des 53 fichiers partagés vérifiés. Validation d’exécution Android en cours. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Gecko PC/Sargo 156.0.1 ; GeckoView Android 156.0. Gaia 2.7.0. L’APK délègue appels/SMS à Android : il ne remplace pas le système téléphonique Android. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Preview expérimentale : Compte Mozilla/Sync, localisation distante, extensions Firefox OS et transfert USB restent incomplets. Sargo : aperçu autour de 16 FPS ; son des vidéos et audio téléphonique à qualifier séparément. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Pixel 3a : Preview 20 r62 démarrée en RAM puis flashée sur boot_a. Empreinte relue identique ; démarrage installé, onze contrôles Gaia et dix contrôles natifs réussis. Données conservées, Wi-Fi connecté et enregistrement LTE observé. Pas de nouvelle conversation audio testée. + + + +## En développement — 12 octobre 2026 — Vidéo et réglages de l’heure + +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Photo : enregistrement WebM VP8/Opus avec autorisation micro, miniature et stockage. Tampon limité à 32 Mio, arrêt avant libération du capteur. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Photo, Galerie et Vidéo : prise en charge des fichiers WebM et de leurs miniatures. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Paramètres : véritables sélecteurs Date/Heure Gaia, retour du clavier intégré et lettres non tronquées dans les listes. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Améliorer Vulpes : lien vers les tickets du projet. Services non raccordés expliqués dans leurs panneaux. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : vidéo simulée 1920 × 1080 relue ; photo, galerie, reprise caméra et navigation vérifiées. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo r61 en RAM : vidéo 480 × 640 relue, huit bascules avant/arrière réussies. Médias de test supprimés ; son enregistré non écouté. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo : aperçu mesuré autour de 16 FPS contre 12 dans le test précédent. 30 FPS restent à atteindre. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : heure automatique par NITZ, réglage persistant et protection des écritures manuelles. Synchronisation dépendante du réseau ; pas de NTP de secours, fuseau choisi manuellement. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Android : ressources préparées et Java compilé, sans nouvel APK ni test vidéo Android à cette étape. Compte Mozilla/Sync, localisation distante, extensions Firefox OS et transfert USB restent à implémenter. Aucun flash ni publication. + + + +## En développement — 12 octobre 2026 — Appels et partage Wi-Fi + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Dialogues Gaia : fermeture après ouverture, transitions interrompues et nouvelle saisie après validation refusée. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : délai des services opérateur augmenté ; réglages lents séparés des commandes voix/SMS. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : double appel, présentation du numéro et quatre règles de renvoi raccordés à oFono, avec validation et relecture. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : point d’accès Wi-Fi WPA2 ou ouvert, configuration NetworkManager et pare-feu adapté. Mot de passe privé, accessible uniquement à Paramètres. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) r59 en RAM : 69 panneaux, dialogues et APN vérifiés sur PC/Pixel. Point d’accès enregistré, activé puis arrêté depuis Gaia ; Wi-Fi précédent restauré. 44 tests Python et régressions des dialogues réussis. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Transit Internet vers un autre appareil non testé. Messagerie vocale, blocage d’appels, PIN/PUK, certificats Wi-Fi et partage USB restent ouverts. Ressources Android synchronisées ; aucun nouvel APK, flash ou publication. + + + +## En développement — 12 octobre 2026 — Paramètres réseau + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Wi-Fi : adresse IPv4 et débit réels ; ne plus afficher « undefined » lorsqu’une mesure manque. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : choix GSM/UMTS/LTE et modification des APN Internet/MMS existants, avec validation et relecture. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) APN : refuser les changements d’un profil actif, protéger IMS et les mots de passe, tenter la restauration après échec et signaler son résultat. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo : file bornée pour les réglages lents. Lecture des appels en 20 ms pendant une attente de réglages de 5 secondes. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) 38 tests Python, 69 panneaux PC et parcours APN/réseau sur Pixel en RAM vérifiés. Ressources Android synchronisées. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Ajout, suppression et réinitialisation des APN indisponibles. Sélection manuelle d’opérateur et certificats encore ouverts. Aucun nouvel APK, flash ou publication. + + + +## En développement — 12 octobre 2026 — Audit des paramètres + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Paramètres : navigation récupérable après échec d’un contrôleur ou d’un dialogue ; génération cohérente des modules. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Filtres d’accessibilité et assistance Vulpes. Panneaux d’origine conservés avec explication des fonctions indisponibles. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) PC : statistiques du profil et téléchargements Gecko. Android : statistiques du stockage privé. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : données mobiles, itinérance, horloge et Wi-Fi masqué raccordés. Erreurs des anciennes interfaces réseau du panneau Son corrigées. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) 97 vues inventoriées ; 69 panneaux PC/Pixel ouverts et sept dialogues natifs vérifiés. Stockage, données et fuseau relus après écriture sur r51 en RAM. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Les vues contextuelles et services indisponibles ne sont pas tous qualifiés. Java Android compilé, sans nouvel APK. Aucun flash, ticket fermé ni publication. + + + +## En développement — 12 octobre 2026 — Navigation des paramètres + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Listes déroulantes : sélecteur Gaia avec confirmation/annulation ; choix Clair/Sombre accessible. Gestionnaire SIM adapté à un emplacement. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Panneaux indisponibles : explication et commandes désactivées au lieu de liens sans effet. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : réseaux Wi-Fi mémorisés séparés des scans lents ; cache conservé lors des lectures d’état. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : informations SIM, centre SMS et accusés de réception reliés au modem. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) r45 en RAM : huit contrôles d’apparence, cinq panneaux et changement réel des accusés SMS validés, puis réglage restauré. Sélecteur PC vérifié ; ressources APK préparées. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Certificats Enterprise et autres options opérateur encore indisponibles. Aucun SMS émis, nouvel APK, flash ou publication. + + + +## En développement — 12 octobre 2026 — Correctifs communs sur Pixel 3a + +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo : boot r39 en RAM avec correctifs partagés et premier mode sombre. Noyau et configuration matérielle du r35 conservés. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Démarrage, taille d’écran, réglages persistants, batterie, interface d’appel et Fira vérifiés. Services modem, audio, Wi-Fi et Bluetooth actifs. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sept contrôles d’apparence réussis dans Gaia : thème propagé, en-têtes lisibles, libellés placés et retour au clair. Capture Pixel inspectée. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Outil de mise à jour Gaia dans un boot privé, avec empreintes, sources conservées et vérification des composants matériels inchangés. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Aucun nouvel appel, SMS ou appairage testé. Thème complet et comportement Android encore à qualifier. Aucun nouvel APK, flash ou publication. + + + +## En développement — 11 octobre 2026 — Apparence et clavier partagé + +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Apparence : Clair, Sombre ou Selon le système, partagé entre les applications et conservé au redémarrage. Fonds d’écran, photos et sites externes préservés. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Clavier : opérations d’édition mutualisées, sélection et effacement, protection contre les réponses tardives. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : neuf contrôles d’apparence et gestes de saisie dans Messages réussis, sans envoi de SMS. Revue visuelle de Paramètres, Messages, Téléphone et Contacts. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Android : option expérimentale de clavier Gaia ; clavier Android conservé par défaut et mode de saisie restauré à la fermeture. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android : réessayer brièvement le même capteur encore occupé, sans réessayer après refus de permission ou fermeture de Photo. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Preview 19 construit localement : signature, alignement, parité et tests du pont/caméra réussis. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Clavier Gaia, caméra avant Android et thème complet encore à qualifier. Sources prêtes pour Sargo, resté sur r35 à cette étape. Aucun upload, flash ni ticket fermé. + + + +## En développement — 11 octobre 2026 — Paramètres et accueil + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Paramètres : gestion des sonneries, saisie/effacement du code PIN, désactivation du verrouillage et liste des permissions des applications tierces. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Accueil : première initialisation du second lanceur et aperçus des fonds d’écran. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Langues : actualiser les libellés Vulpes au changement de langue ; anglais de secours hors français. Fermer le panneau d’échec caméra et les bandeaux d’installation. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Android : ouvrir le volet Gaia en touchant la barre de statut. Preview 18 construit, signé et comparé aux sources. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : neuf parcours Gaia, cinq contrôles de bandeaux et douze tests activités/caméra réussis. Bluetooth Sargo r35 confirmé par l’utilisateur. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Redémarrage à chaud du compositeur encore instable : fichiers restaurés et retour à r35. Corrections non qualifiées sur Sargo à cette étape ; clavier/thème Android ouverts. Aucun APK publié ni flash. + + + +## En développement — 9 octobre 2026 — MMS et Bluetooth + +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Bluetooth : adaptateur Gaia partagé, commandes matérielles séparées et accès limité aux applications autorisées. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : activation, découverte, appairage explicite, connexion et suppression depuis le panneau Bluetooth Gaia. Modules audio Bluetooth intégrés. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : services réseau Qualcomm et transport MMS raccordés. Une déconnexion D-Bus ne termine plus le daemon MMS. État persistant, reprise sans doublon ni renvoi automatique incertain. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) r35 en RAM : MMS avec image visible et accepté par le MMSC Orange ; réception destinataire à confirmer. Cinq appareils Bluetooth détectés, A2DP vers PC validé. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) 191 tests Python Tundra et 18 tests JavaScript réussis. Ramdisk allégé et ressources Android synchronisées. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Audio d’appel via casque non qualifié. Aucun nouvel APK, flash ni push à cette étape. + + + +## En développement — 8 octobre 2026 — Caméra et transport MMS + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Caméra : arrêter le flux avant de détacher la vidéo ; attendre la fermeture du capteur, réinitialiser le viseur et recalculer sa taille après rotation. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) MMS : préserver les pièces jointes Blob et garder les messages en attente jusqu’à confirmation du transport. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo : viseur WebGL réutilisé sans recapture vidéo intermédiaire ; écran maintenu éveillé pendant l’aperçu. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC et Sargo : parcours Photo/Galerie et huit bascules natives vérifiés. JPEG, focus et zoom fonctionnels ; arrière ~16 FPS, avant ~25–27 FPS dans cet essai. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : file MMS persistante, codec Nuntium et transport oFono. Interface texte/photo testée avec transport simulé. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Données mobiles bloquantes pour les MMS opérateur à cette étape. Ressources Android synchronisées ; aucun nouvel APK, flash ou push. + + + +## 8 octobre 2026 — Rappel et messages d’erreur + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Dialogues Gaia : texte visible au lieu d’un écran gris avec un bouton seul. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Journal d’appel : rappel via le même service que le pavé numérique, sans anciennes API Gecko. Fichier partagé PC/Android/Sargo. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) MMS : signaler clairement l’envoi indisponible, garder texte et pièces jointes, ne plus annoncer un faux mode avion. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : dialogues FR/EN et composeur préservé. Sargo en RAM : 11 contrôles Gaia réussis, rappel testé avec modem simulé. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Aucun appel réel ni validation audio dans cette étape. Ressources Android synchronisées, aucun nouvel APK publié. + + + +## En développement — 8 octobre 2026 — SMS et audio + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS : envoyer en un appui en conservant le clavier. Trombone raccordé au menu Gaia : image, audio, vidéo ou contact. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Interface : indicateur de volume visible, gestes du verrouillage et balayage des notifications corrigés. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : clavier, pièces jointes, Galerie, interface d’appel et volume vérifiés ; notification balayée sans déplacer la page. Aucun nouvel APK publié. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : accès à PulseAudio, relais Qualcomm, protocole D-Bus et périphériques de calibration rétablis. Service audio Android concurrent retiré. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : 11 contrôles Gaia, calibration et prévisualisations arrière/avant/arrière réussis. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Appels échouant avant sonnerie avec une erreur réseau dans cet essai. Audio bidirectionnel, geste Pixel et MMS encore non validés. Aucun flash ni publication. + + + +## En développement — 8 octobre 2026 — Fiabilité téléphonie + +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : service radio maintenu après fermeture d’un client. Jamais de nouvel appel ou SMS automatique après une réponse perdue. Erreurs sans numéro ni texte du message. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Diagnostic réseau/audio sans identifiants personnels et paramètres opérateur épinglés dans l’image native. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS : attendre le vrai gestionnaire Gaia et accepter les identifiants oFono avec majuscules. Contrôle des conversations rétabli sans réémission. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : LTE retrouvé après redémarrage, SMS confirmé envoyé par le modem, microphone non silencieux et lecture audio acceptée. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Réception destinataire, audio d’appel bidirectionnel et stabilité modem restent à qualifier. Compositeur instable au redémarrage à chaud. Aucun nouvel APK, flash ou push. ## 8 octobre 2026 — Mise à jour des sources publiques -### Commun · PC / Android / Tundra - -- Regrouper les correctifs récents de caméra, Contacts, SMS, Marketplace et les outils de construction et de restauration Sargo dans les sources publiques. Mettre à jour les README français et anglais avec les capacités et limites vérifiées. -- Corriger le contrôle de parité Android pour créer son dossier de rapports sur une copie neuve du dépôt. Vérifier que la configuration Wi-Fi native embarque un répertoire de connexions vide. -- Conserver les tickets, feuilles de route, documents internes, données personnelles et clés hors du dépôt. - -### Paquets et qualification - -- L’APK public reste Preview17 : les correctifs du 8 octobre nécessitent une prochaine compilation. Aucune nouvelle image native publiée ni flash dans cette étape. -- Sur Sargo, l’enregistrement cellulaire reste en recherche lors du dernier contrôle ; audio bidirectionnel, appairage Bluetooth et profils restent à qualifier. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Publication des correctifs Caméra, Contacts, SMS et Marketplace, et des outils Sargo de construction et de restauration. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) README français et anglais actualisés avec les fonctions et limites vérifiées. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Création du dossier de rapports lors du contrôle de parité Android sur un dépôt neuf. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Configuration Wi-Fi native vérifiée : aucun profil de connexion embarqué. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Documents internes, données personnelles et clés exclus du dépôt. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) APK public inchangé (Preview17) ; correctifs du 8 octobre non inclus. Aucune nouvelle image native publiée. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo : modem encore en recherche réseau ; audio des appels, appairage et profils Bluetooth à valider. ## 8 octobre 2026 — Caméra et parcours Contacts · en développement -### Commun · Gaia / PC / Android / Tundra - -- Sérialiser les ouvertures de caméra, attendre la fermeture du capteur précédent et libérer les pistes et ressources vidéo même après un échec. Stabiliser l’identification avant/arrière sur PC et Android. -- Conserver les images binaires lors des échanges PC/Tundra ; encoder explicitement les photos et dates des contacts pour le pont JSON Android. -- Corriger l’actualisation de la fiche après modification d’un contact. Rétablir les activités Contacts → SMS, sélection d’un numéro et ouverture du pavé numérique, en préservant les demandes imbriquées et les points d’entrée de Gaia. -- Réouvrir Galerie en mode sélection pour ajouter une photo à un contact, même lorsque Galerie est déjà ouverte en consultation. Sélection, recadrage et enregistrement de la photo validés sur PC. -- Ajouter des tests de caméra, de stockage des contacts, de permissions, de transport binaire et de routage des activités. - -### PC - -- Vérifier le parcours original Appareil photo → capture JPEG → Galerie → ouverture de la photo → retour caméra. -- Vérifier dans Gaia : création et modification d’un contact, actualisation de la fiche, recherche sans accents et sans résultats, favoris, destinataire SMS, sélection d’un numéro, pavé numérique prérempli, annulation puis confirmation de suppression. Le test n’émet ni appel ni SMS et supprime son contact temporaire. - -### Tundra · Sargo - -- Vérifier en RAM sur Pixel 3a : autofocus, zoom, quatre alternances avant/arrière avec nouvelles trames, capture JPEG depuis Gaia, stockage, ouverture dans Galerie et arrêt du capteur à l’accueil. -- Vérifier les conversations déjà stockées, le clavier et les notifications. Recherche Wi-Fi : quatre réseaux détectés. Nouvelle émission/réception SMS et audio des appels encore en qualification. -- Ajouter les paquets BlueZ et les bibliothèques nécessaires, verrouillés par SHA256. Le contrôleur Bluetooth démarre, s’active et lance une recherche ; l’appairage depuis Gaia et les profils ne sont pas encore validés. -- Ajouter le magasin de certificats natif : HTTPS vérifié sans désactivation de TLS. Tester les archives altérées, collisions de fichiers et reconstructions répétées. - -- Corriger les fichiers Contacts embarqués dans le build de démarrage : la création, l’édition, le passage vers SMS, le sélecteur et le pavé numérique passent aussi avec les gestes tactiles sur Sargo. - -### Android · APK - -- Publier l’APK Preview17 existant, avec téléchargement et SHA256 vérifiés. Il précède les correctifs du 8 octobre. -- Corriger la transmission des destinataires et du texte de Gaia vers l’application SMS Android, y compris la réponse à une conversation et le numéro saisi sans validation préalable. Tests du pont réussis ; essai Android réel encore nécessaire. -- Réempaqueter les ressources communes corrigées pour la prochaine compilation ; aucun nouvel APK incluant ces correctifs n’est publié. - -**État :** builds natifs de qualification démarrés en RAM. Aucun flash, push de code ni publication de documentation privée dans cette étape. La qualification complète des fonctions de téléphone reste ouverte. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Caméra : ouvertures successives, libération du capteur et identification avant/arrière corrigées. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Contacts : transport des photos et des dates corrigé sur PC, Android et Tundra. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Contacts : actualisation après modification, ouverture de SMS, sélection d’un numéro et préremplissage du pavé numérique rétablis. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Galerie : ouverture en mode sélection pour la photo d’un contact, même si l’application est déjà ouverte. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : fichiers Contacts corrigés inclus dans l’image de démarrage. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android : transmission du destinataire et du texte à l’application SMS, y compris les réponses et les numéros non encore validés. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Tests de caméra, stockage des contacts, permissions, transport binaire et activités. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : capture JPEG, Galerie, photo de contact, création, édition, recherche, favoris et suppression vérifiés. Aucun appel ni SMS émis. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo en RAM : autofocus, zoom, quatre bascules de caméra, capture et Galerie vérifiés ; parcours Contacts testés au toucher. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : conversations stockées, clavier et notifications vérifiés ; quatre réseaux Wi-Fi détectés. Nouveaux SMS et audio d’appel à valider. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : BlueZ et ses dépendances avec empreintes SHA256. Activation et recherche Bluetooth testées ; appairage et profils à valider. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : magasin de certificats natif. HTTPS, archives altérées, collisions de fichiers et reconstructions testés. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Preview17 publié avec téléchargement et SHA256 vérifiés. Les correctifs ci-dessus restent dans les sources ; essai SMS Android réel nécessaire. Aucun flash Sargo. ## 6 octobre 2026 — Thème Marketplace intégré · en développement -### Commun · Gaia / PC / Android / Tundra - -- Ouvrir le catalogue avec `mode=app`, conservé dans les liens, la recherche, les filtres, les fiches et le changement de langue. -- Préparer un thème web Vulpes dédié : Fira locale, navigation compacte, boutons tactiles, cartes lisibles, filtres repliables et captures défilantes. Retirer les en-têtes du site dans le mode intégré, tout en conservant l’interface du site classique. -- Conserver les confirmations et messages système côté Gaia. Le thème ne donne aucun privilège supplémentaire aux pages du catalogue. -- Vérifier 16 pages FR/EN, 2 pages en mode classique et les ressources du thème. Rendus mobiles de 320 et 360 pixels inspectés. Lanceurs PC et Sargo mis à jour ; les sources seront reprises au prochain empaquetage Android. - -Fichiers du site prêts localement, non déployés sur le serveur public. Aucun push ni nouvelle publication APK/ROM. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Marketplace : thème intégré avec Fira locale, navigation compacte, boutons tactiles, filtres repliables et captures défilantes. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Conservation de `mode=app` dans la navigation, la recherche, les filtres et le changement de langue. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) En-têtes du site masqués en mode intégré. Confirmations gérées par Gaia, sans privilèges supplémentaires pour le catalogue. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) 16 pages FR/EN, 2 pages classiques et affichages mobiles à 320 et 360 pixels vérifiés. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Thème prêt localement, non déployé. Lanceurs PC et Sargo mis à jour ; intégration Android au prochain APK. ## 6 octobre 2026 — Bandeaux Gaia · en développement -### Commun · Gaia / PC / Android / Tundra - -- Faire disparaître le bandeau « Application installée » après son délai normal, même si la fin d’animation ne remonte pas du composant Gaia. Conserver son apparence et permettre sa fermeture au toucher. -- Annuler les anciens délais lorsqu’un nouveau message remplace le précédent pour éviter une disparition prématurée. -- Vérifier sur PC l’affichage, l’expiration, le remplacement et la fermeture au clic ; vérifier sur le Pixel le retrait effectif du bandeau après cinq secondes. Correctif déployé sur l’essai Sargo en RAM et disponible dans les sources communes pour le prochain APK. - -Aucun flash ni publication dans cette étape. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Fermeture automatique du bandeau « Application installée », même sans événement de fin d’animation. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Annulation des anciens délais lorsqu’un nouveau bandeau remplace le précédent. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : affichage, expiration, remplacement et fermeture au clic vérifiés. Sargo : disparition après cinq secondes vérifiée en RAM. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Correctif disponible dans les sources communes pour le prochain APK. Aucun flash ni publication. ## 6 octobre 2026 — Wi-Fi et panneaux Paramètres · en développement -### Commun · Gaia / PC / Android / Tundra - -- Terminer l’ouverture d’un panneau Paramètres même si Gecko ne produit pas l’événement de fin d’animation. Conserver les transitions Gaia et éviter les doubles initialisations. -- Correctif dans les sources communes et le serveur PC ; inclus au prochain empaquetage Android. Aucun nouvel APK publié dans cette étape. - -### Tundra · Sargo - -- Corriger les événements de signal Wi-Fi : transmettre le réseau associé et ne pas annoncer de signal de connexion lorsque le téléphone est déconnecté. L’événement incomplet faisait échouer l’affichage des réseaux dans Gaia. -- Sur le Pixel, vérifier le pilote, le service natif, le passage par Gecko et l’affichage du panneau corrigé : quatre points d’accès détectés, regroupés en deux réseaux, sans erreur de recherche. -- Connexion Wi-Fi confirmée par l’utilisateur ; requête HTTPS réelle réussie depuis Gecko vers example.org (HTTP 200, vérification des certificats active). Correctifs appliqués à l’essai en RAM ; aucun flash ni push. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Paramètres : ouverture des panneaux même sans événement de fin d’animation, sans double initialisation. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : événements Wi-Fi corrigés pour rétablir l’affichage des réseaux et signaler correctement la déconnexion. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : quatre points d’accès détectés, regroupés en deux réseaux. Connexion confirmée et requête HTTPS réussie avec certificats vérifiés. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sources communes, serveur PC et essai Sargo en RAM mis à jour. Correctifs non inclus dans l’APK public. ## 6 octobre 2026 — Marketplace · en développement -### PC / Tundra · Sargo - -- Maintenir le catalogue sous la barre de statut et dans les limites de son application. Masquer sa vue web à l’accueil et dans le multitâche ; la retirer à la fermeture de l’application. -- Éviter qu’une navigation annulée ou l’ancienne page d’erreur masque une nouvelle tentative. -- Afficher un message local et proposer une nouvelle tentative lorsque le catalogue est inaccessible, au lieu de laisser sa page d’erreur recouvrir Gaia. -- Sur PC : navigation, erreur réseau, nouvelle tentative, retour à l’accueil et fermeture depuis le multitâche vérifiés. Sept contrôles directs passent sur Sargo en RAM, sans fenêtre de débogage, avec une page locale et une précondition de déverrouillage forcée ; le catalogue distant nécessite une connexion Internet sur le téléphone. - -### Android · APK - -- Reconstruire l’APK local Preview17 avec le message d’erreur Marketplace commun. Signature, alignement et parité des ressources vérifiés ; nouvelle régression sur appareil Android non effectuée dans cette étape. - -Aucune publication dans cette étape. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) PC/Sargo : catalogue Marketplace contenu sous la barre de statut, masqué à l’accueil et retiré à la fermeture. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Une navigation annulée ou une ancienne page d’erreur ne masque plus la nouvelle tentative. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Message d’erreur local avec bouton Réessayer lorsque le catalogue est inaccessible. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : navigation, erreur réseau, nouvelle tentative, accueil et fermeture vérifiés. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo en RAM : sept contrôles réussis avec une page locale et déverrouillage forcé. Catalogue distant non validé sans connexion Internet. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) APK Preview17 local reconstruit ; signature, alignement et ressources vérifiés. Aucun nouveau test sur appareil Android ni publication. ## 2.7-preview.17 — APK construit · 5 octobre 2026 -### Commun · Gaia / PC - -- Remettre Marketplace sur l’accueil avec son icône originale et le catalogue Vulpes. Ouvrir sa version française pour les langues fr, anglaise sinon. -- Adapter les téléchargements du catalogue en installations confirmées : vérifier les archives ZIP, ajouter l’application à l’accueil et permettre son lancement et sa désinstallation. -- Conserver les applications installées séparément des sources Gaia, sur des origines distinctes et sans privilèges système hérités des anciens manifestes. -- Valider sur PC le parcours réel avec 2048 : catalogue, confirmation, installation, lancement, persistance et désinstallation. Contrôler les archives corrompues, chemins invalides, limites, annulation et confirmations à usage unique. - -### Android · APK - -- Construire et signer Preview17 avec le même Marketplace et le même service d’installation. Utiliser la vue GeckoView existante pour le catalogue et le stockage privé Android pour les paquets. -- Vérifier sur émulateur Android 16/API36 le clic Installer, la confirmation, l’apparition sur l’accueil et le lancement de 2048 sans accès au pont système Vulpes. Vérifier aussi la conservation après arrêt forcé et relance, puis la désinstallation. - -### Tundra · Sargo - -- Préparer une image de boot locale incluant Marketplace, l’installateur et le serveur des applications ajoutées. Démarrage en RAM effectué sur le Pixel le 6 octobre : accueil et ressources Marketplace vérifiés, sans flash. Neuf contrôles système sur dix passent ; le contrôle global Gaia relève une variation du viewport au déverrouillage. Installation de paquets sur Sargo encore à qualifier. - -**Limites :** compatibilité initiale avec les applications web empaquetées. Les anciennes API privilégiées, applications hébergées et services disparus ne sont pas restaurés automatiquement. Rien publié sur Forgejo ni sur le site. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Marketplace sur l’accueil, avec son icône originale et le catalogue Vulpes en français ou en anglais. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Installation confirmée des applications ZIP, lancement depuis l’accueil et désinstallation. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Applications installées isolées des sources Gaia et des privilèges système. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : parcours complet avec 2048 et contrôles des archives, chemins, limites, annulations et confirmations à usage unique. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) APK Preview17 construit et signé avec Marketplace, GeckoView et stockage privé des paquets. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Émulateur Android 16/API 36 : installation, lancement, isolation, conservation après redémarrage de l’application et désinstallation de 2048. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo en RAM le 6 octobre : accueil et ressources Marketplace vérifiés, 9 contrôles système sur 10 réussis. Variation du viewport au déverrouillage ; installation de paquets à valider. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Prise en charge initiale des applications web empaquetées. Anciennes API privilégiées et applications hébergées non restaurées. Aucune publication à cette étape. ## 2.7-preview.16 — APK construit · 5 octobre 2026 -### Commun · Gaia / PC - -- Rétablir les sélecteurs Gaia de fond d’écran et de sons, leur retour vers Paramètres et leur annulation par Accueil. Partager le transport des images et sons enregistrés entre PC et Android. -- Propager les changements de langue à l’accueil, y compris sans permission de lecture générale des réglages. Vérifier les libellés FR/EN et la persistance. -- Rétablir l’arrêt du balayage horizontal sur une page et masquer les applications de développement sur l’accueil, sans les supprimer des sources. -- Utiliser le logo Vulpes fourni dans le splash et adapter les composants originaux de la liste des sons au moteur actuel. -- Vérifier sur PC deux changements consécutifs de fond, le choix d’un son et les panneaux Gaia restaurés. - -### Android · APK - -- APK local Preview16 construit et signé ; contenu commun et version vérifiés. Aucune publication dans cette étape. -- Valider Vulpes comme lanceur HOME, afficher le verrouillage Gaia après veille et rétablir le retour Android depuis la caméra ouverte au verrouillage. -- Masquer les barres système Android en mode immersif, en conservant leur accès temporaire par balayage. -- Tests sur émulateur Android 16/API36 : verrouillage, caméra/retour, FR/EN, accueil sans applications de test, fond d’écran et son. Fond conservé après arrêt forcé et relance, sans changement répété pendant 15 secondes. Autres appareils Android non qualifiés dans cette étape. - -### Tundra · Sargo - -- Inclure les mêmes correctifs et la politique des applications dans une nouvelle image de boot de test. Le boot précédent réappliquait une copie ancienne des fichiers et annulait les modifications à chaud. -- Démarrage en RAM sur le Pixel réussi : six contrôles de langues/accueil/sélecteurs et dix contrôles système passent. Sélecteurs image/son vérifiés sans écraser les choix personnels. Le boot installé reste Preview15 ; aucune partition flashée dans cette étape. - -**Limites :** l’installation tierce FFAA reste ouverte. Le site dépend d’anciennes API mozApps et d’un script nomodule ; un installateur isolant les applications et leurs permissions reste à construire. Le choix d’un son Gaia ne modifie pas la sonnerie globale Android ; l’audio d’appel réel n’a pas été qualifié ici. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sélecteurs Gaia de fond d’écran et de sons rétablis, avec retour vers Paramètres et annulation par Accueil. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Changement de langue appliqué à l’accueil et conservé après redémarrage. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Arrêt du balayage horizontal sur chaque page ; applications de développement masquées sur l’accueil. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Logo Vulpes au démarrage et liste des sons adaptée au moteur actuel. Transport des médias partagé entre PC et Android. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android : verrouillage Gaia après veille et retour depuis la caméra du verrouillage. Mode lanceur HOME validé. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Android : barres système masquées en mode immersif, accessibles par balayage. APK Preview16 construit et signé. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC et émulateur Android 16/API 36 : fonds d’écran, sons et panneaux vérifiés ; langue, verrouillage et persistance du fond testés sur Android. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : image de démarrage mise à jour pour ne plus restaurer d’anciens fichiers. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo en RAM : 6 contrôles d’interface et 10 contrôles système réussis. Installation conservée en Preview15, sans flash. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) APK non publié. Installation FFAA et audio d’appel à valider ; le choix d’un son Gaia ne change pas la sonnerie Android. ## En développement — 4 octobre 2026 — Installation autonome Sargo -### Tundra · Sargo - -- Construction d’un volume ext4 neuf directement destiné à userdata : système, données et composants matériels vendor/modem embarqués. Le démarrage ne cherche plus de fichiers dans une installation Droidian ; aucune session Android ni connexion ADB n’est nécessaire à l’installation. -- Installateur Fastboot en Python : contrôle du modèle Sargo, du déverrouillage, des tailles et des empreintes, puis remplacement de userdata, dtbo_a, vbmeta_a et boot_a. Le bootloader reste déverrouillé ; les calibrations propres au téléphone sont conservées. -- Sauvegarde complète de userdata hors ligne, comparaison intégrale avec le téléphone et vérification de l’archive de restauration avant le flash. Les sauvegardes restent privées ; une restauration réelle n’a pas été effectuée dans cette étape. -- Deux démarrages depuis le stockage interne réussis : onze contrôles Gaia et dix contrôles natifs par démarrage, image de boot relue et vérifiée, réglages conservés. Le slot A est marqué réussi uniquement après validation du démarrage Gaia courant. -- Variante générique sans serveur SSH ni clés de maintenance : flash de boot_a et démarrage réel réussis. La variante de développement est ensuite remise sur le Pixel pour poursuivre les travaux. Les images du paquet générique conservent leur état initial vierge. -- Contrôles finaux sur le démarrage autonome de développement : Photo (dont bascules de capteur), clavier Gaia et notifications réussis. -- Paquet Fastboot local assemblé avec empreintes et rapport de qualification. 44 tests ciblés passent. Il n’est pas encore publié : la provenance et la redistribution de certains composants matériels restent à finaliser. -- Limites : qualification sur un Pixel 3a, pas sur toutes les combinaisons de firmware ; volume de 8 Gio comprenant 1 Gio de données modifiables, sans extension automatique. Les fonctionnalités matérielles restent expérimentales. - -### PC / Android · APK - -- Aucun changement d’interface ni nouveau binaire dans cette étape. Gaia reste partagé entre les trois plateformes. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : volume ext4 autonome pour userdata, avec système, données et composants vendor/modem. Installation indépendante de Droidian et d’ADB. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Installateur Fastboot avec contrôles du modèle, du déverrouillage, des tailles et des empreintes. Remplacement de userdata, dtbo_a, vbmeta_a et boot_a ; calibrations conservées. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sauvegarde complète de userdata et archive de restauration vérifiées avant le flash. Restauration réelle non testée. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Deux démarrages installés réussis : 11 contrôles Gaia et 10 contrôles natifs par démarrage, réglages conservés et boot relu conforme. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Variante sans SSH ni clés de maintenance démarrée après flash. Variante de développement réinstallée ; caméra, clavier et notifications vérifiés. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Paquet Fastboot local assemblé ; 44 tests ciblés réussis. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Paquet non publié : provenance et redistribution de composants matériels à finaliser. Essais sur un seul Pixel 3a ; volume de 8 Gio dont 1 Gio modifiable, sans extension automatique. ## En développement — 3 octobre 2026 — Installation Sargo par appareil -### Tundra · Sargo - -- Collecte des six partitions de démarrage en lecture seule et du stockage existant, liés à l’identité du téléphone et au slot A. Les sauvegardes restent privées. -- Dossier appareil portable : construction RAM, contrôle du boot, installation et export du secours peuvent l’utiliser sans l’inventaire partagé du Pixel de développement. Un build lié à un dossier appareil refuse le repli implicite vers cet inventaire. -- Transfert SSH contrôlé : vérification de l’identité et du stockage, contrôle des empreintes et création des fichiers sans remplacer les données existantes. Aucun formatage, flash ou redémarrage dans l’outil de transfert. -- Construction et transfert testés sur le Pixel. Premier boot RAM : onze contrôles Gaia et dix contrôles natifs réussis ; clavier et notifications réussis. Le contrôle caméra a rencontré un délai d’autofocus dépassé, conservé dans le rapport. -- Second boot de la même image : réglage Gaia conservé, onze contrôles Gaia et dix contrôles natifs réussis ; caméra, clavier et notifications réussis. Le timeout autofocus du premier essai reste documenté, pas déclaré corrigé. -- Retour au build installé vérifié : démarrage Gaia réussi et partition de boot inchangée. Un nouveau transfert refuse bien de remplacer les données déjà utilisées. -- Provenance du HAL précisée : paquet et recette de téléchargement identifiés, recettes UBports historiques récupérées. La correspondance exacte avec l’ancien artefact Android et son manifeste de sources reste à établir. -- Prise en charge toujours limitée au noyau qualifié et au stockage ext4/LVM compatible. Ce travail ne fournit pas encore une installation directe depuis Android d’origine. - -### PC / Android · APK - -- Aucun changement d’interface ni nouvelle compilation dans cette étape. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : dossier appareil portable pour les builds, l’installation et le secours, indépendant de l’inventaire du Pixel de développement. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Collecte en lecture seule des six partitions de démarrage et du stockage, liée à l’identité du téléphone. Sauvegardes privées. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Transfert SSH avec contrôle d’identité, du stockage et des empreintes, sans écrasement, formatage ni flash. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Deux boots RAM : 11 contrôles Gaia et 10 contrôles natifs réussis chacun ; réglage conservé. Timeout autofocus au premier essai, caméra fonctionnelle au second. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Retour au build installé vérifié ; boot inchangé et écrasement des données refusé. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Timeout autofocus non déclaré corrigé. Provenance exacte du HAL encore à établir ; noyau et stockage ext4/LVM compatibles requis, sans installation directe depuis Android d’origine. ## En développement — 3 octobre 2026 — Préparation et secours Sargo -### Tundra · Sargo - -- Ajout d’un outil de préparation locale : contrôle du manifeste et de l’archive, décompression bornée, vérification ext4 et création d’un espace de données vierge propre à l’installation. Les dossiers existants ne sont pas écrasés. -- Ajout d’un kit de restauration privé exportable hors du dépôt. Il conserve la sauvegarde originale de boot_a et un outil autonome qui vérifie le modèle, le numéro de série, le slot et les empreintes avant toute restauration explicite. Il ne constitue pas une sauvegarde des données utilisateur. -- La restauration existante ne dépend plus de la présence de l’image de diagnostic. Les contrôles de la sauvegarde sont conservés. -- Construction système : référence matérielle ARM64 désormais explicite, avec refus d’une référence limitée à l’affichage ou dépourvue des services requis. -- Préparation de l’archive système réelle et vérification locale du kit de secours effectuées. Aucun nouveau flash ni nouvelle ROM publiée ; l’installation automatique sur un autre Sargo reste à finaliser. - -### Commun · PC / Android / Tundra - -- Le journal JSON partagé est conservé dans les futures publications de sources ; les documents de travail Markdown restent exclus. - -### PC / Android · APK - -- Aucune modification fonctionnelle ni nouvelle compilation dans cette étape. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : préparation locale des paquets avec contrôles d’empreintes, décompression bornée, vérification ext4 et création de données vierges sans écrasement. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Kit de secours privé avec sauvegarde de boot_a et contrôles du modèle, du numéro de série, du slot et des empreintes. Données utilisateur non incluses. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Restauration possible sans l’image de diagnostic. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Construction système : référence matérielle ARM64 complète exigée. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Archive système préparée et kit de secours vérifié localement. Installation sur un autre Sargo à finaliser ; aucun flash ni ROM publiée. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Journal JSON inclus dans les prochaines publications de sources ; documents de travail exclus. ## En développement — 3 octobre 2026 — Sargo : récupération modem et installation vierge -### Tundra · Sargo - -- Reconstruction de l’image système avec le correctif du cache des polices et la création du répertoire privé d’oFono au premier démarrage. -- Récupération limitée au sous-système modem : test contrôlé avec passage hors ligne, remise en service et réenregistrement réseau en environ 22 secondes, sans redémarrage du téléphone. Cela ne corrige pas la cause du watchdog IMS du firmware. -- Démarrage Gaia, rendu matériel, polices Fira, deux capteurs photo, focus/zoom, clavier et notifications visuelles contrôlés sur le profil neuf. Scan Wi-Fi encore fonctionnel après le redémarrage du modem. -- Deux démarrages réussis avec conservation du même réglage Gaia sur un espace de données séparé. Caméra, clavier et notifications revérifiés au second démarrage. La partition de boot installée et les données existantes sont conservées. -- Aucune nouvelle image native publiée : le paquet public d’installation/restauration reste à terminer. - -### PC / Android · APK - -- Aucun changement de l’interface commune ni nouvelle compilation APK dans cette étape. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : cache des polices et création du dossier privé oFono au premier démarrage corrigés dans l’image système. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Récupération du modem sans redémarrer le téléphone. Réenregistrement réseau mesuré à environ 22 secondes lors d’un essai contrôlé. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Profil neuf : Gaia, rendu matériel, Fira, deux caméras, focus, zoom, clavier et notifications vérifiés. Scan Wi-Fi fonctionnel après redémarrage du modem. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Deux démarrages avec conservation des réglages dans un espace de données séparé. Installation existante préservée. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Cause du watchdog IMS non corrigée. Paquet public d’installation et de restauration encore incomplet ; aucune image publiée. ## 1er octobre 2026 — Sargo : préparation de la distribution — en développement -### Tundra · Sargo - -- Image système Preview 15 reconstruite avec profils vierges, exclusion des documents de travail et conservation des licences ; inventaire des 493 paquets embarqués. -- Vérification ext4 et contrôle du contenu effectués. Ces contrôles ne constituent pas une validation de démarrage. -- Inventaire du stockage en lecture seule et génération de la table de montage depuis cet inventaire, au lieu de reprendre systématiquement les dimensions du Pixel de développement. Prise en charge limitée au volume ext4 à une seule étendue sur userdata Sargo ; aucun formatage ajouté. -- Image nettoyée transférée et vérifiée sur le Pixel chargé. Essais temporaires en RAM, sans écriture de boot_a ; retour automatique vers le build v59 et redémarrage de Gaia contrôlés. -- SIM reconnue, enregistrement LTE et scan Wi-Fi contrôlés sur cette image ; ces contrôles ne valident pas les appels, SMS ou données mobiles de bout en bout. -- Correction des droits du cache des polices au premier démarrage : fc-cache utilise le compte de session. Démarrage Gaia, rendu matériel, Fira, deux capteurs photo, clavier et notifications visuelles contrôlés en RAM. La qualification du fichier reconstruit reste incomplète. -- Redémarrage intermittent identifié dans le firmware IMS du modem ; politique de récupération ciblée appliquée et second démarrage avec LTE, caméra, clavier et notifications réussi. La récupération après un nouveau crash IMS reste non testée. L’image publique reste non flashable et non publiée. - -### PC / APK - -- Aucun changement fonctionnel ni nouvelle compilation APK dans cette étape. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo : image Preview15 reconstruite avec profils vierges, licences et inventaire des 493 paquets. Documents de travail exclus. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Table de montage générée depuis l’inventaire du stockage. Prise en charge limitée à une seule étendue ext4 sur userdata. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Droits du cache des polices au premier démarrage : fc-cache exécuté avec le compte de session. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Contrôles ext4, contenu et transfert réussis. Essais RAM sans écriture de boot_a ; retour au build v59 vérifié. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) SIM, LTE, scan Wi-Fi, Gaia, rendu matériel, Fira, caméras, clavier et notifications vérifiés en RAM. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Récupération ciblée après identification d’un redémarrage intermittent du firmware IMS. Second démarrage réussi ; nouveau crash IMS non testé. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Qualification de l’image incomplète, sans validation des appels, SMS ou données mobiles de bout en bout. Image non publiée et non flashable. ## 2.7-preview.15 — 30 septembre 2026 — publiée +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Première publication des sources PC, Android et Tundra, avec README FR/EN, licences et scripts de préparation. Fichiers privés exclus. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS : dates des bulles et états d’envoi/échec corrigés. Libération et remplacement des flux caméra fiabilisés. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) PC : tests de navigation stabilisés, délai de démarrage à froid ajusté et fermeture propre après dépassement du délai Gaia. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC avec Gecko 156.0.1 : 42 tests de services, démarrage, navigation, réglages, gestes, historique, isolation et persistance vérifiés. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) APK Preview15 signé avec la clé précédente et GeckoView 156.0 ; parité des ressources vérifiée. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Émulateur Android 16 : mise à jour, Photo/Galerie, reprise caméra, dix retours d’arrière-plan et récupération du rendu validés. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo : sources incluses, build privé v59 conservé. Aucune ROM générique ; démarrage lié au stockage et aux images préinstallées. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo : aperçu proche de 16 i/s ; flash, vidéo, rotation et veille profonde incomplets. Audio d’appel et dialogue après envoi SMS à examiner. + [Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15) -### Commun · Gaia / PC / Android / Tundra - -- Première publication des sources communes, avec README français et anglais, licences et scripts de préparation. Documentation interne, roadmap, profils, clés et images privées exclus du dépôt public. -- Intégration des derniers correctifs SMS (dates des bulles, états d’envoi/échec) et du cycle de vie caméra. Les deux entrées du 30 septembre détaillent les correctifs matériels. - -### PC - -- Copie de publication testée avec moteur Gecko 156.0.1 et profil neuf : démarrage Gaia, navigation, réglages, gestes, historique et isolation des pages. 42 tests de services passants ; persistance des services après redémarrage vérifiée. -- Stabilisation du test de navigation lors du remplacement normal d’un document et allongement de l’attente de démarrage à froid. Fermeture propre de l’hôte même si Gaia dépasse le délai de préparation. - -### Android · APK - -- Preview 15 signée avec la clé des précédentes Preview, GeckoView 156.0 ; parité des ressources communes vérifiée. Mise à jour installée sur émulateur Android 16. -- Photo et Galerie : capture, affichage et reprise du capteur validés. Dix passages en arrière-plan et récupération après arrêt du processus d’affichage validés. Cette qualification ne couvre pas tous les téléphones Android. - -### Tundra · Sargo - -- Sources du runtime incluses ; l’image privée v59 reste celle qualifiée sur le téléphone de développement. Aucune ROM générique flashable publiée : démarrage encore lié au stockage inventorié, aux images préinstallées et aux accès de maintenance privés. -- Limites conservées : aperçu caméra proche de 16 i/s, flash/vidéo, rotation et veille profonde incomplets ; audio d’appel à qualifier et grand dialogue après envoi SMS à identifier. - ## En développement — retours SMS et caméra · 30 septembre 2026 -### Commun · Gaia / PC / Tundra - -- SMS : convertir les dates en nombres avant de construire les bulles. Corrige les conversations vides dues à une erreur de formatage de date. Parcours du compositeur et affichage des messages envoyés/reçus vérifiés sur PC avec un modem simulé. -- Photo : fermeture du flux rendue idempotente; détacher les vidéos, attendre la libération du capteur et libérer le contexte WebGL avant de changer de caméra. Identifier chaque session pour qu’un ancien flux ne puisse pas arrêter son remplaçant. Allers-retours arrière/avant vérifiés avec de nouvelles trames à chaque bascule; retour Accueil et arrêt du capteur validés lors de deux démarrages RAM. - -### Tundra / Sargo - -- Vibreur : utiliser le contrôle brightness du pilote DRV2624; la commande legacy activate ne coupait pas physiquement le moteur. Coupure confirmée par l'utilisateur; impulsion, motif 200/200/200 ms et arrêt du service vérifiés, avec retour à zéro. Ajouter une coupure de secours au démarrage et à la sortie du service. -- L'utilisateur confirme focus, zoom et transport SMS dans les deux sens sur v54. Cinq conversations existantes affichées sur le Pixel, avec leurs bulles et sans erreur de date. -- v59 installé sur boot_a après deux démarrages RAM réussis; démarrage installé et relecture vérifiés, slot A marqué réussi. Sources et runtime PC synchronisés. Le grand dialogue après un envoi réel reste à identifier; la fluidité caméra reste un chantier distinct. - -### Android · APK - -- Sources Gaia communes corrigées; aucun nouvel APK construit ou publié. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS : conversion des dates pour corriger les conversations vides. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Caméra : fermeture des flux et libération du capteur corrigées ; une ancienne session ne peut plus fermer la suivante. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : arrêt physique du vibreur via DRV2624, avec coupure de secours au démarrage et à l’arrêt du service. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : composition et affichage des SMS vérifiés avec modem simulé. Sargo : bascules caméra et arrêt du capteur testés sur deux boots RAM. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v54 : focus, zoom et SMS dans les deux sens confirmés par l’utilisateur. Cinq conversations affichées ; arrêt du vibreur confirmé. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo v59 installé sur boot_a après deux boots RAM ; démarrage, relecture et slot A vérifiés. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Dialogue après envoi SMS et fluidité caméra encore à traiter. Sources communes mises à jour, sans nouvel APK. ## En développement — SMS et Photo · 30 septembre 2026 -### PC / Tundra - -- SMS : conserver le message et sa conversation après un refus du modem; transmettre les événements Gaia « envoi », « échec » et « envoyé ». -- SMS : distinguer mise en file oFono et confirmation d'envoi, avec suivi persistant des états terminaux. La confirmation modem n'est pas un accusé de réception du destinataire. -- PC : tests de l'échec d'envoi visible dans la liste des conversations; Photo et Galerie testées avec capture tactile, ouverture, fermeture et reprise de la caméra. - -### Tundra / Sargo - -- Photo : raccorder les zones de focus tactile, le zoom numérique et la capture JPEG du capteur à l'interface Gaia originale. Aperçu conservé en 640 × 480; JPEG arrière 4032 × 3024 (12,19 Mpx) et avant 3264 × 2448 (7,99 Mpx) mesurés directement. Focus fixe à l'avant. -- Protocole caméra : séparer les traces du HAL des données binaires, borner les réponses et relancer l'aperçu après une capture. Une erreur de focus ne doit plus fermer la session. -- SMS : refuser explicitement l'envoi si le modem n'est pas enregistré; conserver les états sent/failed même après disparition de l'objet oFono. La réception a produit des notifications opérateur sur v49 après retour du réseau LTE. -- Qualification : deux démarrages v54 en RAM puis démarrage depuis boot_a validés; image installée sur le Pixel et slot marqué réussi après relecture. Persistance, interface Gaia et tests caméra passants. L'enregistrement réseau reste instable : essais SMS arrêtés avant émission, aucune livraison de bout en bout confirmée. - -### Android · APK - -- Aucun nouvel APK construit ou publié dans cette passe. La Preview 14 disponible reste inchangée. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) PC/Tundra : conservation des SMS après refus du modem et transmission des états envoi, échec et envoyé à Gaia. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : états SMS persistants après disparition de l’objet oFono ; envoi refusé si le modem n’est pas enregistré. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : focus tactile, zoom numérique et capture JPEG du capteur dans Photo Gaia. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Photos mesurées : 4032 × 3024 à l’arrière, 3264 × 2448 à l’avant. Aperçu 640 × 480 ; focus avant fixe. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Caméra native : données binaires séparées des traces HAL, réponses bornées et reprise de l’aperçu après capture ou erreur de focus. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : échec d’envoi visible et parcours Photo/Galerie vérifiés. Sargo v54 : deux boots RAM puis installation, persistance et caméra validées. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Réseau instable ; aucune livraison SMS de bout en bout confirmée dans cette passe. Confirmation modem distincte de l’accusé de réception. APK public inchangé (Preview14). ## 2.7-preview.14 — APK publié · 30 septembre 2026 +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Informations système : alignement des versions et polices Fira dans les composants Shadow DOM. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) PC/Tundra : taille des touches en portrait, curseur dans le champ actif et réponses de saisie obsolètes. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) PC/Tundra : pont clavier Gaia et pavé numérique pour les destinataires SMS. Clavier Android inchangé. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Verrouillage : redimensionnement sans fenêtre clavier. Notifications silencieuses respectées, y compris après restauration. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : notifications persistantes dans le volet et sur le verrouillage, avec ouverture de l’application concernée. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : vibrations annulables, limitées à dix secondes ; profils Wi-Fi privés persistants, reconnexion automatique et oubli du réseau. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : stockage des SMS entrants avant acquittement, dédoublonnage et création du dossier d’état. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : commandes écouteur/haut-parleur, micro muet et volume d’appel. Bascule, sourdine et volume testés hors appel. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Installation : flash des images de diagnostic avec débogueur refusé. Sargo v48 installé après deux boots RAM ; persistance, relecture et slot A vérifiés. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : Calendrier et clavier vérifiés. Sargo : clavier, notifications et reconnexion Wi-Fi vérifiés ; son des notifications et téléphonie réelle à valider. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) APK Preview14 signé et publié. Démarrage, versions et polices vérifiés sur émulateur Android 16. Aucune image Sargo publiée. + [Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.14) -L’APK Preview 14 est publié. La version PC est à jour et le build privé Sargo v48 est installé et testé ; aucune image téléphone n’est publiée avec cette release. - -### Commun · PC / APK / Gaia - -- Informations : suppression du retrait supplémentaire des lignes de version avant « Dernière mise à jour » ; alignement contrôlé sur PC et Android. -- Polices : normalisation des noms de fontes dans les feuilles des composants Shadow DOM, y compris les styles ajoutés après leur création. Les polices d’icônes restent distinctes. -- Calendrier : ouverture du mois et absence d’erreur JavaScript vérifiées sur PC. - -### PC / Tundra - -- Clavier : calculer le mode portrait depuis l’écran, et non depuis la petite fenêtre du clavier. Touches de 60 px CSS environ sur Pixel ; rotation et retour au portrait testés sur PC à densité 3. -- SMS : replacer le curseur dans le champ actif lorsque Gecko conserve la sélection du destinataire ; conserver les sélections valides pour l’édition. -- Notifications Gaia : respecter les indicateurs silencieux, sans son et sans vibration, y compris à la restauration. - -- Clavier Gaia : pont de saisie natif, pavé numérique pour les destinataires SMS, événements de sélection corrigés et protection contre les réponses d’un champ devenu inactif. Saisie destinataire et message testée sur PC et Pixel ; l’envoi opérateur reste à qualifier. -- Verrouillage : accepter l’absence de fenêtre clavier lors d’un redimensionnement. -- Notifications natives : stockage, propriété par application et ouverture du point d’entrée Téléphone déclaré par Gaia. Présentation et suppression dans le volet et sur le verrouillage validées sur Sargo ; écoute du son à qualifier. - -### Tundra / Sargo — build v48 installé - -- Vibrations : pont vers le moteur Sargo, motifs bornés à dix secondes, séquencement sans bloquer les boutons physiques et annulation. Activation et arrêt mesurés sur le pilote. - -- Wi-Fi : profils privés conservés avec reconnexion automatique et commande d’oubli ; tests du stockage et de la restauration en cas d’échec. Connexion et reconnexion après redémarrage RAM vérifiées sur Sargo ; fichiers de profil privés (0600). -- SMS entrants : stockage persistant avant acquittement, dédoublonnage des reprises et services créant leur dossier d’état au démarrage. Défaut de montage du dossier corrigé après reproduction sur Sargo. -- Audio : commandes écouteur/haut-parleur, micro et volume d’appel ajoutées au pont ; bascule écouteur/haut-parleur, sourdine et volume vérifiés sans appel. Conversation dans les deux sens encore à vérifier. -- Images de diagnostic avec débogueur local interdites au flash par l’outil d’installation. v48/preview.14 écrite sur boot_a après deux démarrages RAM et contrôle de persistance ; démarrage installé validé, image relue conforme et slot A marqué réussi. v43 à v48 n’activent pas de débogueur. - -### Android · APK - -- APK preview.14 construit, signé et publié sur Forgejo. Démarrage, informations de version, alignement et sélection des fontes des composants contrôlés sur Android 16 émulé. Le clavier Android reste celui du système. - ## 2.7-preview.13 — parcours dans les applications · 27 septembre 2026 -### Commun · Gaia / PC - -- Calendrier : une alarme planifiée avec succès ne provoque plus un rejet lors de la sauvegarde de l’événement; les erreurs de planification sont transmises au demandeur. -- Rappels : ignorer ceux d’événements supprimés; ne plus marquer une notification refusée comme livrée. La prise en charge des notifications natives Tundra reste à terminer. -- Notifications : accepter un texte brut vide sans chercher une traduction « undefined ». -- Galerie : vignettes de taille positive même au démarrage masqué; annulation des mises à jour d’aperçu après fermeture de l’éditeur. -- SMS : appliquer les filtres correspondant, lu/non lu, période et livraison; tests de lecture et suppression des conversations. -- Revue approfondie : lecture, déplacement et suppression d’une vidéo WebM; édition noir et blanc et copie d’une photo; création et suppression d’un événement. Interfaces Gaia originales conservées. - -- Qualification depuis une copie isolée : démarrage, tests bureau, persistance et empaquetage Android réussis sans réécriture des sources. - -### Android · APK - -- APK preview.13 construit et signé; Photo/Galerie, Calendrier, dix retours d’arrière-plan et récupération du rendu validés sur Android 16 émulé. - -### Tundra · Sargo - -- Build v40/preview.13 installé après deux démarrages RAM : Gaia, caméra, Wi-Fi, luminosité et commandes contrôlés; relecture de boot_a conforme et slot validé. Limites restantes dans `docs/APPLICATION-REVIEW.md`. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Calendrier : sauvegarde des événements avec alarme, remontée des erreurs et exclusion des rappels d’événements supprimés. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Notifications : texte vide accepté ; notification refusée non marquée comme livrée. Support natif Tundra encore incomplet. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Galerie : taille des vignettes au démarrage masqué et arrêt des mises à jour après fermeture de l’éditeur. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS : filtres par correspondant, lecture, période et livraison appliqués. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : vidéo WebM, édition et copie de photo, calendrier, lecture et suppression des conversations vérifiés. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Copie isolée : démarrage, tests PC, persistance et empaquetage Android réussis. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) APK Preview13 construit et signé. Photo/Galerie, Calendrier, dix reprises d’arrière-plan et récupération du rendu validés sur Android 16 émulé. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v40 installé après deux boots RAM ; Gaia, caméra, Wi-Fi, luminosité, commandes et relecture de boot_a vérifiés. ## 2.7-preview.12 — revue des applications · 27 septembre 2026 -### Commun · Gaia / PC - -- Icônes internes : variantes raster Gaia originales de 1× à 2,25× rétablies dans - 1 047 références CSS. Les icônes vectorielles et les polices restent originales. -- Photo : bouton de changement de capteur réactivé après découverte des caméras; - flash indisponible visible et grisé; le pincement ne zoome plus le document Gaia. -- Accueil : gestes natifs réservés au bouton pendant l’appui long; menu contextuel - désactivé sur ce bouton. Le gestionnaire des tâches Gaia reste utilisé. -- Transmission des gestes entre System et les applications restaurée avec des - événements DOM non privilégiés, sans autoriser de faux gestes sur les sites Web. -- Navigateur : intégration de la page `browser-welcome` fournie, locale, bilingue, - sans suivi; recherches et liens transmis au véritable navigateur. -- Page d’accueil : suit automatiquement la langue Gaia, `fr`/`fr-*` en français, - les autres en anglais; changement appliqué sans redémarrage. -- SMS : identifiants compatibles avec le registre numérique Gaia, migration des - anciens messages, dates de l’API et événement d’envoi corrigés; écritures sérialisées. -- Calendrier : libellé d’accessibilité du jour courant retrouvé dans 37 langues. -- Revue de 14 applications; 26 contrôles fonctionnels Galerie/Musique/Horloge/UI, - tests Contacts et SMS, et navigation Web isolée. Voir `docs/APPLICATION-REVIEW.md`. - -### Android · APK - -- APK `2.7-preview.12` : même interface, correctifs et page locale; reconstruction - et parité contrôlées. L’accès caméra reste celui d’Android via getUserMedia. -- Aucun portage automatique des fonctions matérielles Tundra vers Android : chaque - plateforme conserve son adaptateur et annonce ses capacités réelles. - -### Tundra · Sargo - -- Aperçu Photo : conversion NV21 par shader, transport binaire au lieu de base64; - environ 23 images/s mesurées en RAM contre environ 8 dans l’essai intermédiaire. -- Fermeture du worker attendue avant de rouvrir un autre capteur. -- Luminosité manuelle : retrait des animations successives, environ 200 ms pour - atteindre la valeur demandée lors du test matériel; transitions veille/réveil conservées. -- Wi-Fi : état radio réel et activation/désactivation raccordés aux réglages Gaia; - réseau mixte WPA2/WPA3 compatible avec le formulaire WPA-PSK. -- Scan par Gaia, connexion et navigation HTTPS par wlan0 validés en RAM. Les - profils de connexion restent temporaires; leur persistance est encore à réaliser. -- Image v38 installée : deux démarrages RAM, démarrage installé, relecture de - `boot_a`, préservation des autres partitions et validation du slot A réussis. -- Flash photo, vidéo, capteur de luminosité/rotation, Bluetooth, comptage de données - et fonctions avancées d’appel restent incomplets. Un écran qui s’ouvre ne vaut - pas validation de tout son matériel. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Icônes Gaia : variantes de 1× à 2,25× restaurées dans 1 047 références CSS. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Photo : changement de capteur après détection et désactivation du zoom de l’interface au pincement. Flash indisponible grisé. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Accueil : appui long et transmission des gestes rétablis, sans privilèges supplémentaires pour les sites web. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Navigateur : page d’accueil locale bilingue, suivant la langue Gaia sans redémarrage. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS : identifiants, migration, dates, événements d’envoi, requêtes multi-destinataires et écritures concurrentes corrigés. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Calendrier : libellé d’accessibilité du jour courant rétabli dans 37 langues. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Revue de 14 applications ; 26 contrôles Galerie/Musique/Horloge/UI, tests Contacts/SMS et isolation web. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) APK Preview12 reconstruit avec les mêmes ressources ; parité vérifiée. Fonctions matérielles adaptées séparément sur Android et Tundra. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo : aperçu caméra NV21 par shader et transport binaire, environ 23 i/s mesurées contre 8 dans l’essai intermédiaire. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : fermeture du worker caméra attendue avant changement de capteur ; luminosité manuelle réglée en environ 200 ms. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : activation Wi-Fi depuis Gaia et connexion WPA-PSK aux réseaux mixtes WPA2/WPA3. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : scan, connexion et HTTPS validés en RAM. v38 installé après deux boots RAM, relecture et slot A vérifiés. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo : profils Wi-Fi temporaires ; flash, vidéo, capteurs, Bluetooth, comptage de données et appels avancés incomplets. ## En développement — retours Sargo et interface · 27 septembre 2026 -### Commun · Gaia / PC - -- Animation d’arrêt : trois nuances bleues remplacent les ronds orange; dimensions, - séquence et durées de l’animation Gaia sont conservées. - -- Téléphone : empêcher le défilement du document lors du changement d’onglet; - les en-têtes Journal et Contacts restent en place. Les icônes du journal utilisent - les images Gaia originales en plusieurs densités, jusqu’à 2,25×. -- Menus déroulants : retrait du décalage prévu pour les anciens moteurs, qui coupait - les jambages de Fira. Vidéo : texte de présentation contenu dans la fenêtre. -- Luminosité : le curseur enregistre un nombre et répond aux événements input/change; - les anciennes valeurs textuelles sont converties avant la transition Gaia. La file - matérielle regroupe les pas obsolètes de luminosité sans réordonner veille/réveil. -- Accueil : le fond d’écran continue sous le dégradé transparent. L’espace de navigation - reste réservé dans les applications; en paysage, le bouton se place à droite. -- Navigateur : page locale éditable dans `overrides/search.gaiamobile.org/home/index.html`. - Les branchements de Gaia et le mode privé restent en place. -- Wi-Fi : un échec affiche une explication et le bouton de reprise au lieu de relancer - indéfiniment le scanner. La recherche et l’affichage dans le panneau Gaia ont - maintenant été vérifiés sur Sargo en RAM. -- Suivi consolidé : `docs/CHANTIER.md`, avec anciens et nouveaux retours, preuves et limites. - -### Android · APK - -- APK `2.7-preview.11` construit et signé avec la même base Gaia; parité vérifiée. - Barre Accueil à droite en paysage et correction du fond en mode Accueil. - Rotation et absence de recouvrement vérifiées sur émulateur Android 16. - -### Tundra · Sargo - -- Caméra : aperçu matériel dans Photo Gaia, JPEG et bascule arrière/avant validés - en RAM sur Sargo; les capteurs se ferment au retour à l’accueil. Capture initiale - limitée à 640 × 480 (480 × 640 en portrait); vidéo et flash non disponibles. - L’interface reste commune au PC et à l’APK, qui conservent getUserMedia. - -- v33 (`2.7-preview.11`) installé après deux démarrages RAM réussis. Démarrage installé, relecture de - `boot_a`, conservation des données et finalisation du slot A vérifiés. -- Luminosité mesurée depuis Gaia : 35 % → 89/255, 80 % → 204/255. Résultats Wi-Fi - visibles dans le panneau original. Animation d’arrêt bleue incluse. -- Routage audio : prise en charge du format de PulseAudio 14; suppression du double - chargement des modules audio. Passage au profil téléphonique et à l’écouteur, puis - restauration du profil média, vérifiés sur Sargo. Conversation réelle encore à valider. -- Restent ouverts : conversation audible dans les deux sens, capture pleine résolution/vidéo et - raccordement des capteurs pour la rotation automatique. Voir la liste de suivi. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Animation d’arrêt Gaia recolorée en bleu. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Téléphone : en-têtes fixes au changement d’onglet et icônes du journal adaptées aux densités jusqu’à 2,25×. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Texte coupé dans les menus déroulants et débordement du texte d’accueil Vidéo. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Luminosité : valeurs numériques et regroupement des commandes obsolètes, avec ordre veille/réveil préservé. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Accueil : fond sous le dégradé et bouton à droite en paysage, sans recouvrir les applications. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Navigateur : page d’accueil locale modifiable, avec mode privé conservé. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Wi-Fi : message d’erreur et bouton Réessayer à la place des relances sans fin. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) APK Preview11 construit et signé ; rotation et position du bouton Accueil vérifiées sur Android 16 émulé. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : aperçu, JPEG et bascules caméra validés en RAM. Capture limitée à 640 × 480 ; flash et vidéo indisponibles. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v33 installé après deux boots RAM ; démarrage, données, relecture et slot A vérifiés. Luminosité et résultats Wi-Fi contrôlés dans Gaia. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : compatibilité PulseAudio 14 et suppression du double chargement des modules audio. Profils appel/média et écouteur testés. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo : conversation audio, capture pleine résolution, vidéo et rotation automatique à valider. ## En développement — stabilité Wi-Fi native · 27 septembre 2026 -### Commun · Gaia / PC - -- Correction de la boucle Wi-Fi sans délai : recherche toutes les 20 secondes, - conformément à l’intervalle prévu par Gaia. Regroupement des scans simultanés. -- Version de développement commune : `2.7-preview.9`, Gaia `2.7.0`; versions - réelles de Gecko inchangées. Tests de rafales, erreurs et temporisation ajoutés. - -### Android · APK - -- Nouveau paquet `2.7-preview.9` pour conserver la même base Gaia. Le Wi-Fi Android - reste géré par Android; ce correctif ne prétend pas ajouter un pilote à l’APK. - -### Tundra · Sargo - -- Cause du blocage v21/v22 identifiée : multiplication des clients Python de - recherche Wi-Fi, épuisement mémoire et arrêt de Gecko par le lowmemorykiller. - Le SIGSEGV de phoc survient ensuite pendant sa fermeture. -- Un seul scan matériel en cours, cache court des résultats et des erreurs, - invalidation après connexion/déconnexion. Aucun affaiblissement de l’isolation - des applications, ni désactivation du mécanisme de protection mémoire. -- Candidat v25 qualifié sur deux démarrages RAM, puis installé et vérifié sur le - Pixel : 11 contrôles Gaia et 10 contrôles système passent, sans destruction de - processus par manque de mémoire. Verrouillage Gaia, Fira, batterie, date et - panneau Wi-Fi contrôlés; veille/réveil et volume testés par événements injectés. -- Relecture de `boot_a` conforme, autres partitions de démarrage inchangées, - données conservées et slot A marqué comme démarré avec succès. La caméra native - et la validation complète des appels restent à terminer. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Wi-Fi : intervalle de recherche rétabli à 20 secondes et regroupement des scans simultanés. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : un seul scan matériel à la fois, avec cache court et invalidation à la connexion/déconnexion. Corrige l’épuisement mémoire des builds v21/v22. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Tests de rafales, erreurs et temporisation Wi-Fi. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Version commune Preview9, Gaia 2.7.0 ; nouvel APK avec les mêmes sources. Wi-Fi Android toujours géré par Android. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v25 : deux boots RAM puis installation, 11 contrôles Gaia et 10 système réussis, sans arrêt de processus par manque de mémoire. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Verrouillage, polices, batterie, date et Wi-Fi vérifiés ; veille/réveil et volume testés par événements injectés. Données, relecture et slot A contrôlés. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Caméra native et validation complète des appels encore à terminer. ## En développement — restauration Gaia · 27 septembre 2026 -### Commun · Gaia / PC - -- Restauration des panneaux Gaia et suppression des fenêtres/boutons provisoires : - Paramètres, Suivi conso, FM et import de contacts/médias. Les fonctions sans API - restent visibles avec commandes désactivées, sans valeurs matérielles inventées. -- Correction de la portée des anciens styles : les sélecteurs `div` ne sont plus - transformés en racines de composants. Retour des icônes du verrouillage, - conservation de Fira et glyphes Gaia vectoriels pour les onglets Téléphone. -- Accueil dispose de 44 pixels CSS réservés et ne recouvre plus Décrocher/Raccrocher. - La date utilise le composant historique; la batterie est actualisée dans Paramètres - et ses libellés n’affichent plus `{{ level }}`. -- Écran de verrouillage Gaia réactivé; splash provisoire avec le logo fourni, - dimensions stables et animation douce. Nom localisé : Navigateur / Browser. - Remerciements dans Informations légales, avis de licences conservés. -- Tests PC : 26 contrôles applicatifs passent (Galerie, Musique, Horloge, tâches), - panneaux, imports, batterie, date et navigation; appels simulés, - journal, veille/réveil, menu Power et volume. Voir `docs/GAIA-RESTORATION.md`. - -### Android · APK - -- APK `2.7-preview.8` construit et installé sur l’émulateur Android 16, avec la même base Gaia; batterie lue depuis Android, - Paramètres Android en bas de liste et mode Accueil Android optionnel. Le catalogue - des applications Android dans Gaia reste à raccorder. -- Validation Android : dix reprises après changement d’application et récupération - après destruction du renderer. Parité du contenu partagé vérifiée; pas de test - physique Android dans cette passe. -- Les imports utilisent les composants Gaia; le bouton SMS ouvre la confirmation - Android. Le verrouillage interne ne remplace pas la sécurité Android. - -### Tundra · Sargo - -- Candidats v21/v22 construits avec tous les correctifs `overrides/` dans le paquet de boot; - raccordement scan/association Wi-Fi au panneau Gaia via NetworkManager. -- Essais RAM bloqués : service graphique arrêté avec le code 139 avant qualification. - Aucun nouveau flash. Retour sur v20 installé validé (11 contrôles Gaia, 9 système, - données conservées); verrouillage du profil natif remis à son état précédent. - Wi-Fi du candidat, caméra native, API SIM/Bluetooth et suspension CPU non qualifiés. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Panneaux Gaia restaurés : Paramètres, Suivi conso, FM et imports. Interfaces provisoires retirées ; fonctions indisponibles désactivées. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Styles des anciens composants, icônes du verrouillage et glyphes Téléphone corrigés. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Espace de 44 pixels réservé au bouton Accueil ; date et batterie rétablies, sans texte `{{ level }}`. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Verrouillage Gaia réactivé, splash stabilisé, nom Navigateur/Browser localisé et crédits ajoutés. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : 26 contrôles applicatifs réussis, plus panneaux, imports, batterie, navigation et appels simulés. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) APK Preview8 installé sur Android 16 émulé : batterie Android, accès aux paramètres et mode HOME optionnel. Catalogue Android encore incomplet. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Android émulé : dix reprises d’arrière-plan, récupération du rendu et parité des ressources vérifiées. Verrouillage Gaia distinct de la sécurité Android. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo v21/v22 : scan et association Wi-Fi raccordés à Gaia via NetworkManager. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : essais RAM interrompus par l’arrêt du service graphique (code 139). Retour au v20 installé vérifié, sans nouveau flash. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Wi-Fi du candidat, caméra, API SIM/Bluetooth et suspension CPU non validés. ## En développement — 27 septembre 2026 -### Commun · Gaia / PC - -- Remplacement de l’écran d’appel provisoire par **Callscreen**, l’interface - originale de Gaia : appel entrant/sortant, réponse, raccrochage et fermeture. - L’adaptateur fournit les événements `mozTelephony` aux modules historiques. - Les contrôles audio/conférence non encore raccordés restent grisés. -- Journal d’appels persistant côté hôte, puis import dans le vrai journal Gaia, - avec acquittement et détection des doublons après interruption. -- Batterie : pont vers l’icône historique ; une batterie inconnue n’est pas - présentée comme vide. Correction de l’index de contacts Facebook absent ou - mal formé, qui bloquait aussi la recherche de numéro et l’historique local. -- Polices : comparaison des glyphes réellement rendus. L’accueil utilisait - déjà Fira Sans des deux côtés, mais certains textes système utilisaient Noto - sur PC. Les règles de texte principales sélectionnent désormais la Fira de - Gaia ; les polices d’icônes sont conservées. -- Test Gecko avec modem simulé : appels entrants/sortants, boutons Répondre et - Raccrocher, journal, veille/réveil, menu Power, volume et batterie. - Aucun nouvel APK compilé dans cette étape. - -- Étape v13 (remplacée par Callscreen ci-dessus) : écran d’appel géré par l’application système : numérotation, sonnerie, - communication et bouton Raccrocher. Les contrôles restent présents si le - service perd momentanément l’état de l’appel. -- Tests sur le vrai Gecko bureau avec modem simulé, y compris échec puis nouvel - essai de raccrochage. Sources PC synchronisées ; aucun APK reconstruit. - -### Tundra · Sargo - -- Build privé v20 installé et relu dans `boot_a`, après deux démarrages RAM - et un démarrage depuis le stockage ; réglages conservés, slot marqué réussi. -- Lecture des seules touches Power/Volume du Sargo et - transmission à `HardwareButtons`. Appui court : écran éteint/rallumé ; appui - long : menu Gaia. Le broker natif commande rétroéclairage, volume, arrêt et - redémarrage avec une liste d’opérations fixe. -- Sérialisation des changements de luminosité et d’alimentation : deux - requêtes simultanées ne peuvent plus laisser le rétroéclairage éteint au réveil. -- Volume natif : accès en lecture au socket PulseAudio de la session, sans - lequel l’indicateur Gaia changeait mais la sortie audio restait inchangée. -- La veille écran ne suspend pas encore le noyau. Réception réelle des appels, - audio de conversation et commandes audio avancées restent à qualifier. - -- Étape précédente : build privé v13 installé dans `boot_a` après deux démarrages RAM ; démarrage - depuis le stockage, relecture et indicateur de réussite A/B vérifiés. -- Transmission des appels et de leurs états depuis oFono ; commandes Répondre et - Raccrocher limitées aux chemins d’appel du modem. -- Correction des réponses D-Bus sans contenu : un raccrochage réussi ne provoque - plus une fausse erreur de réponse invalide. -- Un appel réel court atteint la sonnerie puis est raccroché automatiquement. - Cela ne qualifie pas encore l’audio en conversation ni les appels entrants. -- Caméra : diagnostic corrigé, sans annoncer une initialisation à partir d’un - simple délai d’attente. Deux capteurs sont détectés via libhybris, mais le test - de flux reçoit zéro image. La caméra Gaia native reste indisponible ; voir la - [note technique](tundra/docs/CAMERA-SARGO.md). +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Callscreen Gaia pour les appels entrants/sortants, avec réponse et raccrochage. Remplace l’écran provisoire v13. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Journal d’appels persistant avec acquittement et dédoublonnage. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Batterie inconnue distincte d’une batterie vide ; index Contacts absent ou mal formé toléré. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Textes système affichés avec Fira Sans, sans modifier les polices d’icônes. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC avec modem simulé : appels, raccrochage en échec puis nouvel essai, journal, veille/réveil, menu Power, volume et batterie. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : boutons Power/Volume raccordés à Gaia et commandes natives d’écran, volume, arrêt et redémarrage. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : luminosité et alimentation sérialisées pour éviter l’écran noir au réveil ; accès PulseAudio corrigé pour le volume. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : un raccrochage réussi avec réponse D-Bus vide ne produit plus de fausse erreur. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v13 puis v20 installés après essais RAM ; démarrage, relecture, réglages et validation du slot contrôlés. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Appel sortant réel arrivé à la sonnerie puis raccroché. Appels entrants et audio de conversation non validés. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Diagnostic caméra : un timeout n’est plus présenté comme une initialisation réussie. Deux capteurs détectés, sans trames reçues. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Caméra native indisponible, suspension CPU et commandes audio avancées incomplètes. Aucun nouvel APK. ## En développement — 26 septembre 2026 -### Commun · Gaia - -- Polices : Fira Sans OpenType du simulateur historique, avec les mêmes fichiers - pour les pages Gaia et le système Tundra. Correction du repli `sans-serif` natif - et vérification Fontconfig ajoutée ; les polices d’icônes restent distinctes. -- Paramètres : retrait des boutons de diagnostic « Paramètres Tundra » et - « Tester une notification ». Le résumé des versions et les informations - techniques restent disponibles. -- Panneau matériel : transmission des capacités de la plateforme au panneau. - Une variable absente provoquait une alerte Gecko au lieu d’afficher la batterie. - -### PC - -- Adaptateur local synchronisé avec les sources communes. Gecko reste en 156.0.1. - -### Android - -- Correctif du panneau présent dans les sources communes ; aucun nouvel APK construit. - -### Tundra / Sargo - -- Validation explicite du succès A/B après installation : sauvegarde et contrôle - des métadonnées GPT, outil Qualcomm vérifié, confirmation Fastboot. -- Erreurs du modem distinguées des erreurs de transport ; boîte d’erreur intégrée - à Gaia. La caméra native indisponible ne suggère plus une autorisation inutile. -- Diagnostic des polices réellement utilisées par Gecko et du réseau radio, sans - appel ni SMS automatique. Fira Sans confirmée sur l’accueil et la barre d’état. -- Installation réelle du v9 polices dans `boot_a` sur le Sargo inventorié. - Contenu écrit relu et conforme ; cinq autres partitions de boot sauvegardées intactes. -- Deux démarrages depuis le stockage : 10 contrôles Gaia et 9 contrôles système - réussis par boot, réglages conservés. Le PC n’est plus nécessaire pour démarrer. - Fichiers de secours Droidian conservés ; restauration réelle non testée. -- Candidat persistant privé : image de données ext4 2 Gio, profils et réglages Gaia - conservés, identité/comptes système préservés et suppression du watchdog de dix minutes. -- Deux démarrages successifs de la même image v8 : 9 contrôles Gaia et 9 contrôles - système réussis par boot. Diagnostic séparé : 15 contrôles avec la même interface. -- Installation limitée à `boot_a` et restauration de l’original préparées et testées - localement ; aucun flash effectué. Les limites matérielles restent documentées. -- Démarrage normal sans ouverture automatique de Paramètres/Horloge ni défilement - de l’accueil. Ces gestes appartiennent désormais au mode explicite `--diagnostics`. -- Plein écran établi avant le chargement de Gaia ; décorations natives désactivées. - Sur le Pixel testé : 360 × 740 pixels CSS, facteur 3, dimensions stables et accueil - inchangé pendant les cinq relevés après démarrage. -- v13 testé en RAM : huit contrôles de démarrage et neuf contrôles système réussis. - Les rapports distinguent cette vérification du démarrage des tests interactifs. -- Diagnostic v14 séparé : treize contrôles interactifs réussis, dont le panneau - matériel ; ouverture et scan Wi-Fi vérifiés. Même interface que le v13 normal. -- Les images de diagnostic interactif ne peuvent pas devenir la sélection normale - du lanceur. L’ancienne sélection v13 est conservée dans son historique. -- Modem enregistré en UMTS pendant l’essai ; lecture audio réussie et signal du - microphone mesuré sans conserver d’enregistrement. Cela ne qualifie pas encore - l’audio en appel, la caméra ou la veille. -- Construction du ramdisk indépendante de la langue du PC : analyse `readelf` en - locale C pour conserver le chargeur dynamique ARM64 et ses dépendances. - -Les six partitions boot/DTBO/vbmeta A/B sont inchangées. Le build reste un essai -privé en RAM, **pas une ROM flashable**. Détails : [état de Tundra](tundra/docs/STATUS.md). +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Polices Fira Sans historiques utilisées dans Gaia et Tundra ; repli sans-serif et contrôle Fontconfig corrigés. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Boutons de diagnostic retirés des Paramètres. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Panneau matériel : capacités de la plateforme transmises correctement pour afficher la batterie. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : erreurs modem et transport distinguées ; message caméra indisponible corrigé. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : données persistantes sur ext4 2 Gio et validation du succès A/B après installation. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v8 : deux boots avec persistance, 9 contrôles Gaia et 9 système par boot. Outils d’installation et de secours testés localement. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v9 installé sur boot_a : deux démarrages avec 10 contrôles Gaia et 9 système chacun. Relecture conforme, cinq autres partitions préservées ; restauration réelle non testée. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Démarrage normal sans gestes de diagnostic automatiques ; plein écran établi avant Gaia, dimensions stables à 360 × 740 pixels CSS. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Mode `--diagnostics` séparé ; images de diagnostic exclues du lancement normal. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : v13 testé en RAM (8 contrôles de démarrage, 9 système), v14 en diagnostic (13 contrôles interactifs), Wi-Fi inclus. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Enregistrement UMTS, lecture audio et signal du micro vérifiés. Audio d’appel, caméra et veille non validés. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Construction du ramdisk indépendante de la langue du PC grâce à readelf en locale C. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Essais privés, sans ROM publique. Sources PC synchronisées ; correctif commun disponible pour Android, sans nouvel APK. ## En développement — 25 septembre 2026 -### Commun · Gaia - -- **Navigation :** bouton Accueil rond ; dégradé noir vers transparent au-dessus du - fond d’écran, fond bleu uni dans les applications. L’appui modifie le cercle - plutôt que de laisser un rectangle bleu sur l’accueil. -- **Défilement :** le dégradé disparaît au bas du panneau de l’accueil ; il revient - quand on remonte. Le changement de panneau Applications / Pages est pris en compte. -- **Typographie :** polices Fira Sans embarquées, avec plusieurs graisses et italiques, - au lieu de dépendre des polices installées sur le système hôte. -- **Informations :** affichage séparé de Vulpes, Gaia, du moteur réellement exécuté, - de la plateforme, du modèle et de l’adaptateur. Le panneau prévoit Tundra et - l’adaptation matérielle, maintenant raccordés au backend natif Tundra. -- **Icônes :** reprise du chargement après une première mesure de taille nulle, - partagée entre PC, Android et Tundra. Le correctif était auparavant limité à Android. -- **Maintenance :** ce journal centralise les évolutions des trois plateformes. - -### PC · Validé localement - -- Chargement d’une icône initialisée à taille nulle puis rendue visible : testé. -- Navigation Accueil / Paramètres / retour Accueil, fond transparent en bas de page, - dégradé en haut et contrôle des permissions testés dans un profil isolé. -- Panneau Informations testé en français et en anglais, sans anciennes lignes en double. -- Le moteur demeure Gecko **156.0.1** ; ces changements d’interface ne recompilent pas Gecko. - -### Android · Sources adaptées, Java compilé - -- Même comportement de navigation prévu par le pont GeckoView / Android : barre - superposée sur l’accueil, place réservée dans les applications et le navigateur. -- État de navigation conservé lors de la recréation de l’activité Android. -- Compilation Java réussie. **Aucun nouvel APK livré pour ces changements** et - comportement visuel encore à retester sur Android. - -### Tundra / Sargo · Essais privés en RAM - -- Build réseau v6 devenu référence du lanceur après 13 contrôles Gaia et 9 - contrôles système ; retour automatique et six partitions inchangées vérifiés. -- Backend Tundra distinct du PC : modèle Pixel 3a, adaptation Sargo et version - native affichés sans annoncer une caméra ou une téléphonie non raccordées. -- Batterie et charge lues sur le matériel ; panneau d’état bilingue dans Gaia. - 13 contrôles Gaia réussis sur l’essai réseau, dont ce nouveau panneau. -- Wi-Fi réel testé : scan, connexion, IPv4, DNS et page HTTPS sous Gecko. - Ajout de `pd-mapper`, désactivation d’IPA non initialisé et du sondage caméra - prématuré ; 9 contrôles système passent, sans Oops noyau. La sélection du réseau - dans Gaia reste à raccorder ; les identifiants du test restent en RAM. -- Configuration des services embarquable dans le petit ramdisk : pas de nouvelle - copie des 3 Gio pour chaque ajustement de démarrage. Partitions modem protégées - en utilisant des copies NV en RAM pour les essais Qualcomm. - -- Nouvelle image système ext4 contenant le runtime ARM64, les HAL de référence, - Phoc, Gecko et Gaia. Le runtime n’est plus chargé depuis l’arborescence de travail - Droidian, ni le HAL depuis son ancien fichier `/var/lib/lxc/android/`. -- Démarrage du second essai vérifié sur Pixel 3a : PID 1 Tundra, services HAL, - compositeur et Gaia ; **7 contrôles Gaia et 8 contrôles système réussis**. -- Capture physique de l’accueil contrôlée : icônes présentes, Fira Sans chargée, - bouton rond et dégradé visibles. Gecko **156.0.1 ARM64**, WebRender matériel. -- Petits changements d’interface embarqués dans le ramdisk d’essai : inutile de - retransférer les 3 Gio du système pour chaque correction CSS/JavaScript. -- Copie des sources de construction conservée avec les nouvelles images, vérifiée - par empreintes, pour permettre de les retester après l’évolution du code courant. -- Le dernier essai (`sargo-system-boot-20260925-v5`) valide **10 contrôles Gaia et - 8 contrôles système**, dont icônes, police chargée, fond à l’appui, bas de page - et retour depuis une application. Capture physique contrôlée ; référence locale - du lanceur mise à jour. Aucun APK publié ni téléphone flashé. - -**Limites :** stockage installé encore utilisé comme support du fichier système ; -modifications perdues à l’arrêt ; retour automatique au système conservé après dix -minutes. Aucun flash ni remplacement de Droidian. Le redémarrage à chaud du -compositeur a échoué lors d’un essai ; le démarrage à froid fonctionne. Réseau, -modem, audio, caméra et veille restent à intégrer ou qualifier. Ce n’est pas une -ROM publique installable ni un téléphone quotidien validé. - -Détails : [image système](tundra/docs/SYSTEM-IMAGE-SARGO.md), -[état Tundra](tundra/docs/STATUS.md), [parité des plateformes](docs/PARITE-BUREAU-ANDROID.md). +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Bouton Accueil rond, dégradé transparent sur le fond d’écran et barre bleue dans les applications. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Dégradé masqué en bas de page et rétabli au défilement ; plus de rectangle bleu à l’appui sur Accueil. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Fira Sans embarquée avec graisses et italiques ; versions produit, Gaia, moteur et plateforme affichées séparément. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Reprise du chargement des icônes après une première mesure de taille nulle sur les trois plateformes. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : icônes, navigation, permissions et panneau Informations FR/EN vérifiés avec Gecko 156.0.1. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android : état de navigation conservé après recréation de l’activité. Compilation Java réussie ; rendu à retester, aucun nouvel APK livré. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Tundra : image ext4 avec runtime ARM64, HAL, Phoc, Gecko et Gaia ; petits correctifs embarqués dans le ramdisk. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : backend dédié, informations Pixel 3a et lecture matérielle de la batterie. Sources de construction archivées avec empreintes. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : WebRender matériel, icônes et polices contrôlés sur capture physique. v5 : 10 contrôles Gaia et 8 système réussis. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : ajout de pd-mapper, désactivation d’IPA non initialisé et du sondage caméra prématuré. Données NV du modem copiées en RAM pour les essais. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v6 : scan Wi-Fi, connexion, IPv4, DNS et HTTPS vérifiés ; 13 contrôles Gaia et 9 système réussis. Sélection du réseau dans Gaia encore à raccorder. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Essais RAM sans flash, retour automatique après dix minutes et modifications perdues à l’arrêt. Démarrage à froid réussi ; redémarrage à chaud du compositeur en échec. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Pas de ROM publique ; modem, audio, caméra et veille encore à intégrer ou valider. ## 2.7-preview.7 — APK construit, référence locale -### Commun / PC / Android - -- Correction de l’autorisation interne d’ouverture du capteur sur PC et de la - reprise après la première demande de permission CAMERA sur Android. -- Messages Photo distincts pour un refus, un capteur absent ou un échec d’ouverture. -- Version produit commune **2.7-preview.7**, Gaia **2.7.0** ; numéros du moteur séparés. -- Gecko PC **156.0.1**, GeckoView Android **156.0** : l’écart réel de correctif est - affiché, sans renommer artificiellement l’un des moteurs. - -Détails : [Photo et Galerie](docs/CAMERA-GALERIE.md), [README Android](android/README.md). +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Permission caméra sur PC et reprise après la première autorisation Android. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Messages distincts pour une permission refusée, un capteur absent et un échec d’ouverture. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Versions produit et moteur séparées : Vulpes Preview7, Gaia 2.7.0, Gecko PC 156.0.1 et GeckoView Android 156.0. ## 2.7-preview.6 — APK construit -### Commun / PC / Android - -- Portage de la véritable application Photo Gaia : aperçu et capture JPEG via - webcam sur PC ou capteur via GeckoView sur Android. -- Photos enregistrées dans Galerie Gaia ; import de médias par le sélecteur du système. -- Commandes non prises en charge désactivées : notamment flash et vidéo ; changement - de caméra selon les capteurs disponibles. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Photo Gaia sur PC et Android : aperçu et capture JPEG via webcam ou GeckoView. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Enregistrement dans Galerie et import par le sélecteur du système. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Flash et vidéo désactivés ; changement de caméra selon les capteurs disponibles. ## 2.7-preview.5 — APK construit -### Commun - -- Versions centralisées dans `release.json`, séparation produit / Gaia / moteur. -- Interface des fonctions natives partagée entre PC et Android ; contrôle du contenu - commun réellement embarqué dans l’APK avant distribution. - -### PC - -- Intégration des panneaux de réglages Linux disponibles, import de médias et vCard, - notifications du bureau et capture webcam de test. - -### Android - -- Même interface Gaia et commandes natives que le PC, adaptées aux capacités Android. -- Tests de retour au premier plan et de récupération après arrêt du processus de rendu. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Versions centralisées dans `release.json` ; produit, Gaia et moteur séparés. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Interface native commune PC/Android et contrôle de parité des ressources de l’APK. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) PC : réglages Linux, imports médias/vCard, notifications et capture webcam de test. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Android : retour au premier plan et récupération après arrêt du processus de rendu. ## 2.7-156.0.2 — APK historique construit (révision 4) -Ce nom historique mélangeait révision du produit et version du moteur ; il ne -signifie pas que l’APK utilisait Gecko 156.0.2. Le moteur Android était Gecko 156.0. - -### Android - -- Reprise de l’activité et des sessions pour limiter les écrans blancs au retour dans Vulpes. -- Accès aux réglages Android, au volume et à la luminosité de la fenêtre. -- Import de médias et contacts, notifications natives avec permissions. -- Transfert au composeur, à SMS et à Horloge Android ; aucun envoi silencieux ni - remplacement des applications système Téléphone/SMS. -- Capture initialement déléguée à l’application photo Android, avant le portage de Photo Gaia. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android : reprise de l’activité et des sessions pour réduire les écrans blancs. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Accès aux réglages Android, volume et luminosité de la fenêtre. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Import de médias et contacts ; notifications natives avec permissions. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Transfert vers Téléphone, SMS, Horloge et Appareil photo Android. Aucun envoi silencieux ni remplacement des applications par défaut. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Numérotation historique : moteur Android Gecko 156.0, malgré le nom 2.7-156.0.2. ## 2.7-156.0.1 — APK publié (révision 3) -- Nom **Vulpes OS - Preview** et icône du projet. -- Correctif du fond d’écran clignotant inclus. -- Numérotation historique conservée ; ne pas en déduire le patch du moteur Android. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Nom « Vulpes OS - Preview » et icône du projet. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Clignotement du fond d’écran corrigé. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Numérotation historique, distincte de la version du moteur Android. [Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1) @@ -830,7 +549,7 @@ signifie pas que l’APK utilisait Gecko 156.0.2. Le moteur Android était Gecko ## 2.7-preview.2 — APK publié -- Correction du fond d’écran clignotant dans la Preview Android. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Clignotement du fond d’écran dans la Preview Android. [Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.2) @@ -838,8 +557,8 @@ signifie pas que l’APK utilisait Gecko 156.0.2. Le moteur Android était Gecko ## 2.7-preview.1 — APK publié -- Première Preview Android embarquant Gaia et GeckoView. -- Version archivée ; clignotement du fond d’écran connu. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Première Preview Android avec Gaia et GeckoView. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Version archivée ; clignotement du fond d’écran connu. [Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.1) @@ -847,26 +566,12 @@ signifie pas que l’APK utilisait Gecko 156.0.2. Le moteur Android était Gecko ## Étapes antérieures — rétrospective technique -Ces étapes ne correspondent pas à de nouvelles releases publiques datées. - -### Gaia / PC - -- Constitution de Vulpes à partir de Gaia historique ; identité visuelle, fond - d’écran, traductions et travail sur les informations système. -- Portage expérimental Gecko 45 → 52, puis séparation de Gaia, des services et du - moteur pour l’hôte Gecko récent. -- Réimplémentation des API nécessaires : réglages, registre d’applications, - contacts locaux, messages, alarmes, stockage et communications entre applications. -- Adaptation des composants visuels anciens, des interactions tactiles simulées, - du navigateur et des applications ; essais d’applications Firefox OS archivées. -- Outillage de mise à jour Gecko avec vérifications, moteur candidat et retour - arrière explicite. Il détecte les incompatibilités, sans les réparer automatiquement. -- Lancement et mise à jour de Gaia sans recompilation complète du moteur. - -### Tundra / Sargo - -- Inventaire du Pixel 3a, sauvegardes des partitions de boot et tests USB de diagnostic. -- Tests QEMU, puis Gecko ARM64 dans Droidian et sous un compositeur dédié. -- Racine distincte, services HAL sélectionnés et PID 1 Tundra ; essais graphiques - temporaires par Fastboot avec secours SSH et retour automatique. -- Noyau de référence **4.9-124-google-sargo**, adaptation **Android 9 / API 28**. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Base Gaia historique avec identité Vulpes, fond d’écran, traductions et informations système. +- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Portage expérimental Gecko 45 vers 52, puis séparation de Gaia, des services et du moteur. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) API de réglages, applications, contacts, messages, alarmes, stockage et communications entre applications. +- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Adaptation des composants Gaia, gestes tactiles, navigateur et applications aux moteurs récents. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Mise à jour Gecko avec contrôles et retour arrière ; mise à jour Gaia sans recompilation du moteur. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Applications Firefox OS archivées, QEMU, puis Gecko ARM64 dans Droidian et sous compositeur dédié. +- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : inventaire, sauvegardes de boot, racine distincte, services HAL et PID 1 Tundra. +- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Essais graphiques Fastboot temporaires avec secours SSH et retour automatique. +- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Noyau 4.9-124-google-sargo, adaptation Android 9/API 28. Étapes historiques, sans nouvelles releases datées. diff --git a/README.fr.md b/README.fr.md index fe75f913..92c85dc5 100644 --- a/README.fr.md +++ b/README.fr.md @@ -4,7 +4,7 @@

Firefox OS fork · Open source

-Vulpes: 2.7 Preview 17 +Vulpes: 2.7 Preview 20 Gaia: 2.7 Gecko: 156.0.1 Tundra: 0.1.0 @@ -38,6 +38,12 @@ Je développe Vulpes principalement seul, sous le nom d’OverSu, avec l’aide 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. +### Preview 20 · 13 octobre 2026 + +[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. + +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é. + ### Linux Prérequis : Linux x86_64, Git, Python 3 et les bibliothèques système nécessaires à Firefox. diff --git a/README.md b/README.md index a3779e29..72bee2ba 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

Firefox OS fork · Open source

-Vulpes: 2.7 Preview 17 +Vulpes: 2.7 Preview 20 Gaia: 2.7 Gecko: 156.0.1 Tundra: 0.1.0 @@ -38,6 +38,12 @@ I’m OverSu, and I develop Vulpes mostly on my own, with help from people who t 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. +### Preview 20 · 13 October 2026 + +[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. + +Desktop/Sargo use Gecko **156.0.1**; Android uses GeckoView **156.0**. Android 8 / API 26 minimum is declared; not every device is tested. Calls and SMS open Android applications. The APK is a signed development build, with debugging through authorized ADB. + ### Linux You’ll need Linux x86_64, Git, Python 3 and the system libraries required by Firefox. diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1d1d8db4..fae6411a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + diff --git a/android/app/src/main/java/org/vulpes_os/preview/AndroidServices.java b/android/app/src/main/java/org/vulpes_os/preview/AndroidServices.java index f52c11c4..64020f99 100644 --- a/android/app/src/main/java/org/vulpes_os/preview/AndroidServices.java +++ b/android/app/src/main/java/org/vulpes_os/preview/AndroidServices.java @@ -102,6 +102,18 @@ final class AndroidServices { ); result.complete(null); break; + case "android.storage": + io.execute(() -> { + try { + File data = new File(app.getApplicationInfo().dataDir); + File mediaRoot = new File(app.getFilesDir(), "media"); + JSONObject stats = new JSONObject().put("free", data.getUsableSpace()) + .put("apps", storageBytes(data, mediaRoot)).put("sdcard", storageBytes(mediaRoot, null)) + .put("scope", "vulpes-profile"); + main.post(() -> result.complete(stats)); + } catch (Exception e) { fail(result, e); } + }); + break; case "android.media": JSONObject params = p; io.execute(() -> { @@ -122,6 +134,15 @@ final class AndroidServices { return result; } + private static long storageBytes(File file, File skip) throws IOException { + if (file.equals(skip) || !file.exists() || !file.getCanonicalFile().equals(file.getAbsoluteFile())) return 0; + if (file.isFile()) return file.length(); + long size = 0; + File[] children = file.listFiles(); + if (children != null) for (File child : children) size += storageBytes(child, skip); + return size; + } + private void fail(GeckoResult result, Exception e) { main.post(() -> { try { diff --git a/android/app/src/main/java/org/vulpes_os/preview/MainActivity.java b/android/app/src/main/java/org/vulpes_os/preview/MainActivity.java index 5b4dcba7..640a5578 100644 --- a/android/app/src/main/java/org/vulpes_os/preview/MainActivity.java +++ b/android/app/src/main/java/org/vulpes_os/preview/MainActivity.java @@ -550,12 +550,17 @@ public final class MainActivity extends Activity { "http".equals(origin.getScheme()) && "camera.localhost".equals(origin.getHost()) && origin.getPort() == 18765; - if (!camera || (audio != null && audio.length > 0) || video == null || video.length == 0) { + boolean hasVideo = video != null && video.length > 0; + boolean hasAudio = audio != null && audio.length > 0; + if (!camera || (!hasVideo && !hasAudio)) { callback.reject(); return; } - permission(new String[] { android.Manifest.permission.CAMERA }, granted -> { - if (granted) callback.grant(video[0], null); + java.util.ArrayList 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(); }); } diff --git a/android/extension/background.js b/android/extension/background.js index e189b1d5..4baab916 100644 --- a/android/extension/background.js +++ b/android/extension/background.js @@ -1,3 +1,4 @@ +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'; @@ -10,6 +11,8 @@ 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; @@ -31,7 +34,7 @@ function grants(app) { function broadcast(type, data, target) { for (const peer of peers) { if (target && peer.app.id !== target) continue; - if (type === 'settingchange' && data.key !== 'language.current' && !grants(peer.app).includes('settings.read')) continue; + if (type === 'settingchange' && !['language.current', 'vulpes.theme'].includes(data.key) && !grants(peer.app).includes('settings.read')) continue; if ( type === 'contactchange' && !peer.app.manifest.permissions?.contacts?.access?.includes('read') @@ -90,6 +93,7 @@ const ready = (async () => { 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( @@ -100,7 +104,12 @@ const ready = (async () => { const next = { ...values, ...Object.fromEntries(entries) }; await browser.storage.local.set({ settings: next }); values = next; - for (const [key, value] of entries) broadcast('settingchange', { key, value }); + for (const [key, value] of entries) { + broadcast('settingchange', { key, value }); + if(key==='keyboard.gaia.enabled') { + input.hide();broadcast('input-mode',{enabled:value===true}); + } + } }); writes = write.catch(() => {}); return write; @@ -302,6 +311,9 @@ async function handle(peer, request) { } if (!Object.keys(params.values).length) return null; } + if (method === 'input.config') return values['keyboard.gaia.enabled'] === true; + if (method === 'input.request') return input.request(peer,params); + if (method === 'appearance.current') return values['vulpes.theme'] || 'light'; if (method === 'locale.current') return values['language.current'] || 'en-US'; if (method === 'settings.snapshot') { if (!grants(app).includes('settings.read')) throw Error('PERMISSION_DENIED'); @@ -357,6 +369,7 @@ async function handle(peer, request) { 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); @@ -412,6 +425,7 @@ async function handle(peer, request) { return null; } if (method === 'desktop.wakelock') return null; // Android Activity owns the visible screen. + if (method === 'storage.stats') {if(app.id!=='settings')throw Error('PERMISSION_DENIED');return android('storage',{});} if (method === 'media.request') return media(app, params); throw Error('METHOD_NOT_SUPPORTED'); } @@ -449,6 +463,15 @@ browser.runtime.onConnect.addListener((port) => { app, manager: { documentURI: { spec: url.href } }, sendAsyncMessage: (_, event) => notify(peer, event.type, event.data), + edits: new Map(), + sendQuery(_, params) { + const id=++editSequence; + return new Promise((resolve,reject)=>{ + const timer=setTimeout(()=>{peer.edits.delete(id);reject(Error('INPUT_CONTEXT_EXPIRED'));},5000); + peer.edits.set(id,{resolve,reject,timer}); + port.postMessage({edit:{id,params}}); + }); + }, }; peers.add(peer); }); @@ -456,6 +479,16 @@ browser.runtime.onConnect.addListener((port) => { let reply; try { await attached; + if(Object.hasOwn(message,'inputFocus')) { + if(values['keyboard.gaia.enabled']===true) input.changed(peer,message.inputFocus); + return; + } + if(Object.hasOwn(message,'editReply')) { + const edit=peer.edits.get(message.editReply); + if(edit) {clearTimeout(edit.timer);peer.edits.delete(message.editReply); + if(message.error)edit.reject(Error(message.error));else edit.resolve(message.result);} + return; + } reply = { result: await handle(peer, message.request) }; } catch (e) { reply = { error: { code: e.message || 'HOST_ERROR' } }; @@ -469,6 +502,9 @@ browser.runtime.onConnect.addListener((port) => { attached .then(() => { peers.delete(peer); + 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); diff --git a/android/extension/content.js b/android/extension/content.js index 17bbcc85..4c1b7d5c 100644 --- a/android/extension/content.js +++ b/android/extension/content.js @@ -25,7 +25,23 @@ }); } 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') { + gaiaKeyboard=message.event.data.enabled===true; + if(!gaiaKeyboard) restoreMode(); + win.VulpesNativeInput=gaiaKeyboard; + win.dispatchEvent(new window.Event('vulpes-input-configured')); + } win.dispatchEvent( new window.CustomEvent('vulpes-service-event', { detail: cloneInto(message.event, win) }), ); @@ -50,6 +66,45 @@ }, 1000); } connect(); + const editor = new InputEditor(window); + let gaiaKeyboard = false; + 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); + } + request({version:1,id:0,method:'input.config',params:{}}).then(reply => { + gaiaKeyboard = reply.result === true; + win.VulpesNativeInput = gaiaKeyboard; + win.dispatchEvent(new window.Event('vulpes-input-configured')); + }).catch(error => console.error('Gaia input configuration:', error)); + for (const type of ['focusin', 'pointerdown', 'click']) addEventListener(type, event => { + if (!gaiaKeyboard || editor.editing || location.hostname === 'keyboard.localhost') return; + let element = event.target; + if (element?.isContentEditable) element=element.closest('[contenteditable=true]') || element; + const editable = element?.isContentEditable || + (['input','textarea'].includes(element?.localName) && + !['button','submit','range','checkbox','radio','date','time','datetime-local','file'].includes(element.type)); + if (!editable || element.disabled || element.readOnly) { + if (type === 'click' && !element?.closest?.('[data-vulpes-keep-input]')) port.postMessage({inputFocus:null}); + return; + } + 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}); + }, true); + addEventListener('visibilitychange', () => { + if (document.hidden && gaiaKeyboard) { restoreMode(); port.postMessage({inputFocus:null}); } + }); addEventListener('unload', () => clearTimeout(reconnect)); exportFunction(request, win.navigator, { defineAs: 'vulpesRequest' }); const frames = new Map(); diff --git a/android/extension/manifest.json b/android/extension/manifest.json index 3220ceb0..740241d3 100644 --- a/android/extension/manifest.json +++ b/android/extension/manifest.json @@ -25,6 +25,7 @@ "http://*.localhost/*" ], "js": [ + "input-editor.js", "content.js" ], "all_frames": true, diff --git a/android/tools/check-camera.py b/android/tools/check-camera.py index 636d32ce..a6da92f7 100644 --- a/android/tools/check-camera.py +++ b/android/tools/check-camera.py @@ -125,11 +125,11 @@ result["camera_resumes_from_android_background"] = wait( "camera", '!document.hidden && !!app.camera.mozCamera && app.camera.mozCamera!==__streamBefore && app.camera.mozCamera.getVideoTracks().every(t=>t.readyState==="live") && !app.camera.isBusy', ) -result["unavailable_controls"] = ev( +result["capability_controls"] = ev( "camera", - '[".js-flash",".mode-switch"].every(s=>document.querySelector(s).getAttribute("aria-disabled")==="true")', + 'document.querySelector(".js-flash").getAttribute("aria-disabled")==="true" && (document.querySelector(".mode-switch").getAttribute("aria-disabled")!=="true") === !!Object.keys(app.camera.mozCamera.capabilities.recorderProfiles).length', ) -assert result["unavailable_controls"] +assert result["capability_controls"] result["errors"] = ev( "camera", '__vulpesDiagnostics.filter(d=>d.kind==="error" || d.kind==="rejection")' ) diff --git a/android/tools/pack-assets.py b/android/tools/pack-assets.py index e6c9e1ed..5f004300 100644 --- a/android/tools/pack-assets.py +++ b/android/tools/pack-assets.py @@ -12,7 +12,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "tools")) -from project import build_info, validate_assets, release_info, application_manifest +from project import SOURCE_OVERRIDES, build_info, validate_assets, release_info, application_manifest validate_assets() OUT = ROOT / "android/app/src/main/assets" @@ -46,17 +46,8 @@ info = build_info() release = release_info() revision = info["revision"] commit_time = str(info["timestamp"]) -source_overrides = { - "system": [ - "js/launcher.js", - "js/gaia_scheduler.js", - "js/keyboard_manager.js", - "js/core.js", - "js/rocketbar.js", - ] -} textsuffix = {".html", ".js", ".css", ".json", ".webapp", ".properties", ".svg"} -head = b'' +head = b'' if "--bridge-only" not in sys.argv or not (OUT / "gaia.zip").exists(): with zipfile.ZipFile(OUT / "gaia.zip", "w", zipfile.ZIP_DEFLATED, compresslevel=6) as z: hashes = {} @@ -82,7 +73,7 @@ if "--bridge-only" not in sys.argv or not (OUT / "gaia.zip").exists(): if p.is_file() and p.name != "manifest.webapp" } ) - for name in source_overrides.get(app["id"], []): + for name in SOURCE_OVERRIDES.get(app["id"], []): files[name] = ROOT / "gaia/apps" / app["id"] / name for name, p in files.items(): data = p.read_bytes() @@ -120,7 +111,7 @@ if "--bridge-only" not in sys.argv or not (OUT / "gaia.zip").exists(): data = translate(data.decode("utf8")).encode() put("_shared/" + p.relative_to(ROOT / "gaia/shared").as_posix(), data) put("_vulpes/platform-config.js", b'window.VulpesPlatform="android";') - for name in ["gaia-compat.js", "gaia-services.js", "gaia-styles.js", "gaia-restoration.js", "gaia-wifi.js", "desktop.css", "platform-ui.js", "gaia-camera.js"]: + for name in ["gaia-compat.js", "gaia-input.js", "gaia-services.js", "gaia-styles.js", "gaia-theme.js", "gaia-select.js", "gaia-mobile-settings.js", "gaia-theme.css", "gaia-restoration.js", "gaia-wifi.js", "gaia-bluetooth.js", "desktop.css", "platform-ui.js", "gaia-camera.js"]: text = ( (ROOT / "host" / name) .read_text() @@ -145,6 +136,7 @@ for name in ["Connections", "Contacts", "Messages"]: ) (ext / f"{name}.mjs").write_text(text) shutil.copy2(ROOT / "host/Alarms.sys.mjs", ext / "Alarms.mjs") +(ext / "input-editor.js").write_text((ROOT / "services/input-editor.mjs").read_text().replace('export class InputEditor', 'class InputEditor')) shutil.copy2(ROOT / "host/marketplace-page.js", ext / "marketplace-page.js") (ext / "release.json").write_text(json.dumps(release)) (ext / "registry.json").write_text(json.dumps(registry, ensure_ascii=False)) diff --git a/assets/webapps/camera.gaiamobile.org/js/main.js b/assets/webapps/camera.gaiamobile.org/js/main.js index 6ed05434..1d04d37a 100644 --- a/assets/webapps/camera.gaiamobile.org/js/main.js +++ b/assets/webapps/camera.gaiamobile.org/js/main.js @@ -5447,6 +5447,9 @@ define('views/viewfinder', [ if (!preview) { return; } + // Recompute when the embedded app becomes visible or rotates. + this.getSize(); + if (!this.width || !this.height) { return; } var aspect; if (sensorAngle % 180 === 0) { this.container = { @@ -5697,6 +5700,7 @@ define('controllers/viewfinder', [ this.views.viewfinder.set('grid', 'off'); }; ViewfinderController.prototype.bindEvents = function () { + window.addEventListener('resize', this.configurePreview); this.views.viewfinder.on('fadedin', this.app.firer('viewfinder:visible')); this.views.viewfinder.on('fadedout', this.app.firer('viewfinder:hidden')); this.views.viewfinder.on('click', this.app.firer('viewfinder:click')); diff --git a/assets/webapps/settings.gaiamobile.org/js/panels/app_permissions_list/panel.js b/assets/webapps/settings.gaiamobile.org/js/panels/app_permissions_list/panel.js index 86119b44..773df448 100644 --- a/assets/webapps/settings.gaiamobile.org/js/panels/app_permissions_list/panel.js +++ b/assets/webapps/settings.gaiamobile.org/js/panels/app_permissions_list/panel.js @@ -1,7 +1,7 @@ -define('modules/navigator/mozPermissionSettings',[],function(){'use strict';return window.navigator.mozPermissionSettings;});define('panels/app_permissions_list/app_permissions_list',['require','modules/settings_service','shared/manifest_helper','modules/apps_cache','modules/navigator/mozApps','modules/navigator/mozPermissionSettings'],function(require){'use strict';var SettingsService=require('modules/settings_service');var ManifestHelper=require('shared/manifest_helper');var AppsCache=require('modules/apps_cache');var mozApps=require('modules/navigator/mozApps');var mozPerms=require('modules/navigator/mozPermissionSettings');var PermissionsList=function pl(){this._listRoot=null;this._permissionsTable=null;this._permissionTableHaveProcessed=false;this._apps=null;this._enabled=false;};PermissionsList.prototype={init:function pl_init(listRoot){this._listRoot=listRoot;this._boundOnAppChoose=this._onAppChoose.bind(this);this._boundOnApplicationInstall=this._onApplicationInstall.bind(this);this._boundOnApplicationUninstall=this._onApplicationUninstall.bind(this);},set enabled(value){if(value!==this._enabled){this._enabled=value;if(this._enabled){this._bindEvents();}else{this._unbindEvents();}}},_bindEvents:function pl__bindEvents(){this._listRoot.addEventListener('click',this._boundOnAppChoose);AppsCache.addEventListener('oninstall',this._boundOnApplicationInstall);AppsCache.addEventListener('onuninstall',this._boundOnApplicationUninstall);},_unbindEvents:function pl__unbindEvents(){this._listRoot.removeEventListener('click',this._boundOnAppChoose);AppsCache.removeEventListener('oninstall',this._boundOnApplicationInstall);AppsCache.removeEventListener('onuninstall',this._boundOnApplicationUninstall);},setPermissionsTable:function pl_setPermissionsTable(permissionTable){this._permissionTable=permissionTable;},refresh:function pl_refresh(){var self=this;this._apps=[];if(this._permissionTableHaveProcessed){return this.loadApps();}else{return mozApps.getSelf().then(function(app){return self._initExplicitPermissionsTable(app);}).then(function(){return self.loadApps();});}},_onAppChoose:function pl__onAppChoose(evt){if(evt.target.dataset&&evt.target.dataset.appIndex){SettingsService.navigate('appPermissions-details',{app:this._apps[evt.target.dataset.appIndex],permissionsTable:this._permissionTable});}},_onApplicationInstall:function pl__onApplicationInstall(evt){var app=evt.application;this._apps.push(app);this._sortApps();this.renderList();},_onApplicationUninstall:function pl__onApplicationUninstall(evt){var app;var appIndex;this._apps.some(function findApp(anApp,index){if(anApp.origin===evt.application.origin){app=anApp;appIndex=index;return true;} +define('modules/navigator/mozPermissionSettings',[],function(){'use strict';return window.navigator.mozPermissionSettings;});define('panels/app_permissions_list/app_permissions_list',['require','modules/settings_service','shared/manifest_helper','modules/apps_cache','modules/navigator/mozApps','modules/navigator/mozPermissionSettings'],function(require){'use strict';var SettingsService=require('modules/settings_service');var ManifestHelper=require('shared/manifest_helper');var AppsCache=require('modules/apps_cache');var mozApps=require('modules/navigator/mozApps');var mozPerms=require('modules/navigator/mozPermissionSettings');var PermissionsList=function pl(){this._listRoot=null;this._permissionsTable=null;this._permissionTableHaveProcessed=false;this._apps=null;this._enabled=false;};PermissionsList.prototype={init:function pl_init(listRoot){this._listRoot=listRoot;this._boundOnAppChoose=this._onAppChoose.bind(this);this._boundOnApplicationInstall=this._onApplicationInstall.bind(this);this._boundOnApplicationUninstall=this._onApplicationUninstall.bind(this);},set enabled(value){if(value!==this._enabled){this._enabled=value;if(this._enabled){this._bindEvents();}else{this._unbindEvents();}}},_bindEvents:function pl__bindEvents(){this._listRoot.addEventListener('click',this._boundOnAppChoose);AppsCache.addEventListener('oninstall',this._boundOnApplicationInstall);AppsCache.addEventListener('onuninstall',this._boundOnApplicationUninstall);},_unbindEvents:function pl__unbindEvents(){this._listRoot.removeEventListener('click',this._boundOnAppChoose);AppsCache.removeEventListener('oninstall',this._boundOnApplicationInstall);AppsCache.removeEventListener('onuninstall',this._boundOnApplicationUninstall);},setPermissionsTable:function pl_setPermissionsTable(permissionTable){this._permissionTable=permissionTable;},refresh:function pl_refresh(){var self=this;this._apps=[];if(this._permissionTableHaveProcessed){return this.loadApps();}else{return mozApps.getSelf().then(function(app){return self._initExplicitPermissionsTable(app);}).then(function(){return self.loadApps();});}},_onAppChoose:function pl__onAppChoose(evt){var target=evt.target.closest('[data-app-index]');if(target&&this._listRoot.contains(target)){SettingsService.navigate('appPermissions-details',{app:this._apps[target.dataset.appIndex],permissionsTable:this._permissionTable});}},_onApplicationInstall:function pl__onApplicationInstall(evt){var app=evt.application;this._apps.push(app);this._sortApps();this.renderList();},_onApplicationUninstall:function pl__onApplicationUninstall(evt){var app;var appIndex;this._apps.some(function findApp(anApp,index){if(anApp.origin===evt.application.origin){app=anApp;appIndex=index;return true;} return false;});if(!app){return;} -SettingsService.navigate('appPermissions');this._apps.splice(appIndex,1);this.renderList();},_sortApps:function pl__sortApps(){this._apps.sort(function alphabeticalSort(app,otherApp){var manifest=new ManifestHelper(app.manifest?app.manifest:app.updateManifest);var otherManifest=new ManifestHelper(otherApp.manifest?otherApp.manifest:otherApp.updateManifest);return manifest.name>otherManifest.name;});},_genAppItemTemplate:function pl__genAppItemTemplate(itemData){var icon=document.createElement('img');var item=document.createElement('li');var link=document.createElement('a');var span=document.createElement('span');span.textContent=itemData.name;span.dir='auto';icon.src=itemData.iconSrc;link.dataset.appIndex=itemData.index;link.href='#';link.classList.add('menu-item');link.appendChild(icon);link.appendChild(span);item.appendChild(link);return item;},renderList:function pl_renderList(){this._listRoot.innerHTML='';var listFragment=document.createDocumentFragment();this._apps.forEach(function appIterator(app,index){var manifest=new ManifestHelper(app.manifest?app.manifest:app.updateManifest);var li=this._genAppItemTemplate({name:manifest.displayName,index:index,iconSrc:this._getBestIconURL(app,manifest.icons)});listFragment.appendChild(li);}.bind(this));this._listRoot.appendChild(listFragment);},_initExplicitPermissionsTable:function pl__initExplicitPermissionsTable(app){this._currentApp=app;var table=this._permissionTable;table.explicitCertifiedPermissions=[];table.plainPermissions.forEach(function plainPermIterator(perm){if(this._isExplicitPerm(perm)){table.explicitCertifiedPermissions.push({explicitPermission:perm,permission:perm});}}.bind(this));table.composedPermissions.forEach(function permIterator(perm){table.accessModes.forEach(function modeIterator(mode){var composedPerm=perm+'-'+mode;if(this._isExplicitPerm(composedPerm)){table.explicitCertifiedPermissions.push({explicitPermission:composedPerm,permission:perm});}}.bind(this));}.bind(this));this._permissionTableHaveProcessed=true;},_isExplicitPerm:function pl_isExplicitPerm(perm){return mozPerms.isExplicit(perm,this._currentApp.manifestURL,this._currentApp.origin,false);},loadApps:function pl_loadApps(){var self=this;return AppsCache.apps().then(function(apps){self._loadApps(apps);});},_loadApps:function pl__loadApps(apps){var table=this._permissionTable;apps.forEach(function(app){var manifest=app.manifest?app.manifest:app.updateManifest;if(manifest.type!='certified'){this._apps.push(app);return;} +SettingsService.navigate('appPermissions');this._apps.splice(appIndex,1);this.renderList();},_sortApps:function pl__sortApps(){this._apps.sort(function alphabeticalSort(app,otherApp){var manifest=new ManifestHelper(app.manifest?app.manifest:app.updateManifest);var otherManifest=new ManifestHelper(otherApp.manifest?otherApp.manifest:otherApp.updateManifest);return manifest.name>otherManifest.name;});},_genAppItemTemplate:function pl__genAppItemTemplate(itemData){var icon=document.createElement('img');var item=document.createElement('li');var link=document.createElement('a');var span=document.createElement('span');span.textContent=itemData.name;span.dir='auto';icon.src=itemData.iconSrc;link.dataset.appIndex=itemData.index;link.href='#';link.classList.add('menu-item');link.appendChild(icon);link.appendChild(span);item.appendChild(link);return item;},renderList:function pl_renderList(){this._listRoot.innerHTML='';var listFragment=document.createDocumentFragment();this._apps.forEach(function appIterator(app,index){var manifest=new ManifestHelper(app.manifest?app.manifest:app.updateManifest);var li=this._genAppItemTemplate({name:manifest.displayName,index:index,iconSrc:this._getBestIconURL(app,manifest.icons)});listFragment.appendChild(li);}.bind(this));this._listRoot.appendChild(listFragment);},_initExplicitPermissionsTable:function pl__initExplicitPermissionsTable(app){this._currentApp=app;var table=this._permissionTable;table.explicitCertifiedPermissions=[];table.plainPermissions.forEach(function plainPermIterator(perm){if(this._isExplicitPerm(perm)){table.explicitCertifiedPermissions.push({explicitPermission:perm,permission:perm});}}.bind(this));table.composedPermissions.forEach(function permIterator(perm){table.accessModes.forEach(function modeIterator(mode){var composedPerm=perm+'-'+mode;if(this._isExplicitPerm(composedPerm)){table.explicitCertifiedPermissions.push({explicitPermission:composedPerm,permission:perm});}}.bind(this));}.bind(this));this._permissionTableHaveProcessed=true;},_isExplicitPerm:function pl_isExplicitPerm(perm){return mozPerms&&mozPerms.isExplicit(perm,this._currentApp.manifestURL,this._currentApp.origin,false);},loadApps:function pl_loadApps(){var self=this;return AppsCache.apps().then(function(apps){self._loadApps(apps);});},_loadApps:function pl__loadApps(apps){var table=this._permissionTable;apps.forEach(function(app){var manifest=app.manifest?app.manifest:app.updateManifest;if(manifest.type!='certified'){this._apps.push(app);return;} var display=table.explicitCertifiedPermissions.some(function iterator(perm){var permInfo=mozPerms.get(perm.explicitPermission,app.manifestURL,app.origin,false);return permInfo!='unknown';}.bind(this));if(display){this._apps.push(app);}}.bind(this));this._sortApps();this.renderList();},_getBestIconURL:function pl__getBestIconURL(app,icons){if(!icons||!Object.keys(icons).length){return'../style/images/default.png';} var preferredIconSize=30*(window.devicePixelRatio||1);var preferredSize=Number.MAX_VALUE;var max=0;for(var size in icons){size=parseInt(size,10);if(size>max){max=size;} if(size>=preferredIconSize&&size0x39)){return;} -var key=String.fromCharCode(code);if(evt.charCode===0){if(this._passcodeBuffer.length>0){this._passcodeBuffer=this._getPasscodeBuffer('all');if(this._elements.panel.dataset.passcodeStatus==='success'){this._resetPasscodeStatus();}}}else if(this._passcodeBuffer.length<8){this._passcodeBuffer+=key;} -this._updatePassCodeUI();this._processPasscode();},_updateHeaderAndButtonUI:function(){var headerL10nId=this._headerL10nIdMapping[this._mode];var buttonL10nId=this._submitButtonL10nIdMapping[this._mode];if(headerL10nId){this._elements.header.setAttribute('data-l10n-id',headerL10nId);} +define('panels/screen_lock_passcode/panel',['require','modules/settings_utils','modules/dialog_panel','shared/passcode_helper'],function(require){'use strict';var SettingsUtils=require('modules/settings_utils');var DialogPanel=require('modules/dialog_panel');var PasscodeHelper=require('shared/passcode_helper');return function ctor_screenlockPasscode(){var mozSettings=window.navigator.mozSettings;return DialogPanel({_elements:{},_mode:'create',_leftApp:false,_passcodeBuffer:'',_checkingLength:{'create':8,'new':8,'edit':4,'confirm':4,'confirmLock':4},_headerL10nIdMapping:{'create':'new-passcode','edit':'current-passcode','new':'new-passcode','confirm':'enter-passcode','confirmLock':'enter-passcode'},_submitButtonL10nIdMapping:{'new':'change','create':'create','edit':'','confirm':'','confirmLock':''},onInit:function(panel){this._elements.panel=panel;this._elements.gaiaHeader=panel.querySelector('gaia-header');this._elements.header=panel.querySelector('gaia-header h1');this._elements.passcodeInput=panel.querySelector('.passcode-input');this._elements.passcodeDigits=panel.querySelectorAll('.passcode-digit');this._elements.passcodeContainer=panel.querySelector('.passcode-container');this._elements.submitButton=panel.querySelector('button[type="submit"]');this._elements.submitButtonText=this._elements.submitButton.querySelector('span');this._bindEvents();},_bindEvents:function(){this._elements.passcodeInput.type='password';this._elements.passcodeInput.inputMode='numeric';this._elements.passcodeInput.maxLength=8;this._elements.passcodeInput.oninput=this._onPasscodeInput.bind(this);this._elements.passcodeContainer.onclick=this._onPasscodeContainerClick.bind(this);},_onPasscodeContainerClick:function(evt){evt.preventDefault();this._elements.passcodeInput.focus();},_onPasscodeInput: function() { + // Android IMEs commit text without legacy keypress/charCode events. + var value = this._elements.passcodeInput.value.replace(/[^0-9]/g, '').slice(0, 8); + this._hideErrorMessage(); + if (this._elements.panel.dataset.passcodeStatus === 'success') { + this._resetPasscodeStatus(); + } + this._passcodeBuffer = value; + this._updatePassCodeUI(); + this._processPasscode(); + },_updateHeaderAndButtonUI:function(){var headerL10nId=this._headerL10nIdMapping[this._mode];var buttonL10nId=this._submitButtonL10nIdMapping[this._mode];if(headerL10nId){this._elements.header.setAttribute('data-l10n-id',headerL10nId);} if(buttonL10nId===''){this._elements.submitButton.disabled=true;this._elements.submitButtonText.removeAttribute('data-l10n-id');this._elements.submitButtonText.textContent='';}else if(buttonL10nId){this._elements.submitButton.disabled=false;this._elements.submitButtonText.setAttribute('data-l10n-id',buttonL10nId);} -SettingsUtils.runHeaderFontFit(this._elements.gaiaHeader);},_updatePassCodeUI:function(){for(var i=0;i<8;i++){if(i{if(result){mozSettings.createLock().set({'lockscreen.passcode-lock.enabled':false}).then(()=>{this._showSuccessPasscodeStatus();this.cancel();}).catch((error)=>{console.log(error);});}else{this._passcodeBuffer='';this._updatePassCodeUI();}});break;case'confirmLock':this._checkPasscode().then((result)=>{if(result){mozSettings.createLock().set({'lockscreen.enabled':false,'lockscreen.passcode-lock.enabled':false}).then(()=>{this._showSuccessPasscodeStatus();this.cancel();}).catch((error)=>{console.log(error);});}else{this._passcodeBuffer='';this._updatePassCodeUI();}});break;case'edit':this._checkPasscode().then((result)=>{if(result){this._passcodeBuffer='';this._updatePassCodeUI();this._showDialogInMode('new');}else{this._passcodeBuffer='';this._updatePassCodeUI();}});break;}}},_showDialogInMode:function(mode){this._mode=mode;this._elements.panel.dataset.mode=mode;this._hideErrorMessage();this._updatePassCodeUI();this._updateHeaderAndButtonUI();},onSubmit:function(){if(this._elements.panel.dataset.passcodeStatus!=='success'){this._showErrorMessage();this._elements.passcodeInput.focus();return Promise.reject();}else{var originalPasscode=this._getPasscodeBuffer('original');return mozSettings.createLock().set({'lockscreen.passcode-lock.enabled':true}).then(()=>{return PasscodeHelper.set(originalPasscode);});}},onCancel:function(){this._elements.passcodeInput.blur();},onBeforeShow:function(panel,options){this._mode=options.mode;if(!this._leftApp){this._showDialogInMode(this._mode);} this._leftApp=false;},onShow:function(){this._elements.passcodeInput.focus();},onHide:function(){this._leftApp=document.hidden;if(!this._leftApp){this._passcodeBuffer='';}},_checkPasscode:function(){return PasscodeHelper.check(this._passcodeBuffer).then((result)=>{if(result){this._hideErrorMessage();}else{this._showErrorMessage();} return result;}).catch(()=>{this._showErrorMessage();});},_showErrorMessage:function(){this._elements.panel.dataset.passcodeStatus='error';},_hideErrorMessage:function(){this._elements.panel.dataset.passcodeStatus='';},_resetPasscodeStatus:function(){this._elements.panel.dataset.passcodeStatus='';},_showSuccessPasscodeStatus:function(){this._elements.panel.dataset.passcodeStatus='success';},_getPasscodeBuffer:function(part){if(part==='original'){return this._passcodeBuffer.substring(0,4);}else if(part==='confirm'){return this._passcodeBuffer.substring(4,8);}else if(part==='all'){return this._passcodeBuffer.substring(0,this._passcodeBuffer.length-1);}else{console.log('You are using _getPasscodeBuffer wrongly, '+'please check your argument again - '+part);}}});};}); \ No newline at end of file diff --git a/assets/webapps/verticalhome.gaiamobile.org/gaia_build_defer_index.js b/assets/webapps/verticalhome.gaiamobile.org/gaia_build_defer_index.js index e56f7a77..7aa6a9b6 100644 --- a/assets/webapps/verticalhome.gaiamobile.org/gaia_build_defer_index.js +++ b/assets/webapps/verticalhome.gaiamobile.org/gaia_build_defer_index.js @@ -399,7 +399,7 @@ function iteratorSV(value){this.applicationSource.addPreviouslyInstalledSvApp(va loadTable(DB_SV_APP_STORE_NAME,'indexSV',iteratorSV.bind(this));loadTable(DB_ITEM_STORE,'index',iterator,finish.bind(this));function addIfUnique(item){if(!cached[item.identifier]){cached[item.identifier]=true;this._allItems.push(item);}} function finish(){for(var i=0,iLen=collected.length;i{if(event.data.action!=='migrate'){console.error('Unhandled migration action requested:',event.data.action);return;} this.migrate(event.data.manifestURL);};port.start();},migrate:function(manifestURL){this.migratingTo=manifestURL;console.info('verticalhome migrating to '+this.migratingTo);if(this.migrating){return;} this.migrating=true;(datastore?Promise.resolve():navigator.getDataStores(DATASTORE_NAME).then(stores=>{datastore=stores[0];return Promise.resolve();},(e)=>{return Promise.reject(e);})).then(()=>{this.all(entries=>{this.saveMigrationData(entries,'detail',true).then(()=>{this.migrating=false;window.navigator.mozSettings.createLock().set({'homescreen.manifestURL':this.migratingTo});navigator.getDataStores(PREFS_STORE).then(stores=>{if(stores.length<1){console.error('Error opening verticalhome prefs datastore');return;} diff --git a/assets/webapps/wallpaper.gaiamobile.org/style/wallpaper.css b/assets/webapps/wallpaper.gaiamobile.org/style/wallpaper.css index 0a581104..51e131ed 100644 --- a/assets/webapps/wallpaper.gaiamobile.org/style/wallpaper.css +++ b/assets/webapps/wallpaper.gaiamobile.org/style/wallpaper.css @@ -70,3 +70,6 @@ html[dir="rtl"] .wallpaper { margin: 0 0.5rem; } + +/* Keep preview tiles aligned when source wallpapers have different ratios. */ +#wallpapers .wallpaper { aspect-ratio: 2 / 3; object-fit: cover; display: block; } diff --git a/changelog.json b/changelog.json index b2ab60b7..96d2f307 100644 --- a/changelog.json +++ b/changelog.json @@ -1,239 +1,873 @@ { "schema": 1, "preamble": { - "fr": "# Journal des évolutions! — Vulpes OS\n\n[English](CHANGELOG.en.md)\n\nUne base Gaia, trois plateformes : **PC**, **Android (APK)** et **Tundra / Sargo**.\nCe journal décrit les changements visibles et les étapes techniques importantes.\nLes rapports détaillés restent dans la documentation de chaque plateforme.\n\n**Statuts :** « En développement » concerne les sources locales, « APK construit »\nun paquet de test, et « Publié » une release accessible dans le dépôt. Un correctif\ncommun n’apparaît dans un APK installé ou une image téléphone qu’après leur mise à jour.\n\n

\"Les

\n

Paramètres de Gaia · capture de la version pour ordinateur.

", - "en": "# Changelog — Vulpes OS\n\n[Français](CHANGELOG.md)\n\nOne Gaia base, three platforms: **desktop**, **Android (APK)** and **Tundra / Sargo**.\nThis journal covers visible changes and major technical milestones.\n\n**Status:** “In development” refers to local sources, “APK built” to a test package,\nand “Published” to a public repository release. Shared source changes reach installed\nAPKs and phone images only after those packages are updated.\n\n

\"Gaia

\n

Gaia Settings · screenshot from the desktop version.

" + "fr": "# Journal des modifications — Vulpes OS\n\n[English](CHANGELOG.en.md)\n\nPC, Android (APK) et Tundra/Sargo. Les statuts indiquent des sources en développement, un APK construit ou une version publiée. Les changements des sources nécessitent une mise à jour du paquet installé.\n", + "en": "# Changelog — Vulpes OS\n\n[Français](CHANGELOG.md)\n\nDesktop, Android (APK) and Tundra/Sargo. Status indicates development sources, a built APK or a published release. Source changes require an update to the installed package.\n" }, "entries": [ + { + "id": "2.7-preview.20", + "text": { + "fr": "## 2.7-preview.20 — APK construit · 13 octobre 2026\n\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Preview 20 réunit les corrections communes depuis l’APK Preview 17 : paramètres Gaia, apparence, sélecteurs, clavier, contacts, Marketplace et médias.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Photo : enregistrement WebM avec miniature et lecture dans Galerie/Vidéo. Autorisation micro demandée ; commandes adaptées aux capacités du matériel.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Caméra : changement de capteur, arrêt et reprise fiabilisés. PC et Android utilisent leurs API caméra ; Sargo conserve son adaptateur HAL.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo : mises à jour Wi-Fi, Bluetooth, appels, SMS/MMS et réglages réseau incluses dans les sources. Les fonctions non raccordées restent indiquées dans les panneaux.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Changelog : reprise des textes simplifiés FR/EN, badges conservés, filtres PC/APK/Sargo indépendants des titres. Markdown, site et notes de release issus du même JSON.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) APK signé avec la clé des Preview précédentes ; alignement et cohérence des 53 fichiers partagés vérifiés. Validation d’exécution Android en cours.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Gecko PC/Sargo 156.0.1 ; GeckoView Android 156.0. Gaia 2.7.0. L’APK délègue appels/SMS à Android : il ne remplace pas le système téléphonique Android.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Preview expérimentale : Compte Mozilla/Sync, localisation distante, extensions Firefox OS et transfert USB restent incomplets. Sargo : aperçu autour de 16 FPS ; son des vidéos et audio téléphonique à qualifier séparément.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Pixel 3a : Preview 20 r62 démarrée en RAM puis flashée sur boot_a. Empreinte relue identique ; démarrage installé, onze contrôles Gaia et dix contrôles natifs réussis. Données conservées, Wi-Fi connecté et enregistrement LTE observé. Pas de nouvelle conversation audio testée.\n", + "en": "## 2.7-preview.20 — APK built · 13 October 2026\n\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Preview 20 brings together shared fixes since APK Preview 17: Gaia settings, appearance, selectors, keyboard, contacts, Marketplace and media.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Camera: WebM recording with posters and Gallery/Video playback. Microphone permission requested; controls follow hardware capabilities.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Camera: more reliable sensor switching, stopping and resuming. Desktop and Android use their camera APIs; Sargo retains its HAL adapter.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo: Wi-Fi, Bluetooth, calls, SMS/MMS and network settings updates included in the sources. Unimplemented functions remain identified in their panels.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Changelog: simplified French/English text and badges retained; desktop/APK/Sargo filters independent of headings. Markdown, website and release notes generated from one JSON.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) APK signed with the previous Preview key; alignment and consistency of 53 shared files verified. Android runtime validation in progress.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Desktop/Sargo Gecko 156.0.1; Android GeckoView 156.0. Gaia 2.7.0. The APK delegates calls/SMS to Android; it does not replace Android telephony.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Experimental Preview: Mozilla account/Sync, remote location, Firefox OS add-ons and USB file transfer remain incomplete. Sargo: preview around 16 fps; recorded sound and call audio require separate qualification.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Pixel 3a: Preview 20 r62 RAM-booted, then flashed to boot_a. Matching readback hash; installed startup, eleven Gaia checks and ten native checks passed. Data retained, Wi-Fi connected and LTE registration observed. No new two-way audio call tested.\n" + }, + "metadata": { + "kind": "version", + "status": "built", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] + } + }, + { + "id": "development-2026-10-12-video-clock", + "text": { + "fr": "## En développement — 12 octobre 2026 — Vidéo et réglages de l’heure\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Photo : enregistrement WebM VP8/Opus avec autorisation micro, miniature et stockage. Tampon limité à 32 Mio, arrêt avant libération du capteur.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Photo, Galerie et Vidéo : prise en charge des fichiers WebM et de leurs miniatures.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Paramètres : véritables sélecteurs Date/Heure Gaia, retour du clavier intégré et lettres non tronquées dans les listes.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Améliorer Vulpes : lien vers les tickets du projet. Services non raccordés expliqués dans leurs panneaux.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : vidéo simulée 1920 × 1080 relue ; photo, galerie, reprise caméra et navigation vérifiées.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo r61 en RAM : vidéo 480 × 640 relue, huit bascules avant/arrière réussies. Médias de test supprimés ; son enregistré non écouté.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo : aperçu mesuré autour de 16 FPS contre 12 dans le test précédent. 30 FPS restent à atteindre.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : heure automatique par NITZ, réglage persistant et protection des écritures manuelles. Synchronisation dépendante du réseau ; pas de NTP de secours, fuseau choisi manuellement.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Android : ressources préparées et Java compilé, sans nouvel APK ni test vidéo Android à cette étape. Compte Mozilla/Sync, localisation distante, extensions Firefox OS et transfert USB restent à implémenter. Aucun flash ni publication.\n", + "en": "## In development — 12 October 2026 — Video and clock settings\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Camera: WebM VP8/Opus recording with microphone permission, poster and storage. Buffer limited to 32 MiB; recording stops before releasing the sensor.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Camera, Gallery and Video: handle WebM files and their posters.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Settings: original Gaia date/time pickers, Back from built-in keyboard settings and unclipped select labels.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Improve Vulpes: link to project issues. Unavailable services explained in their panels.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: simulated 1920 × 1080 video decoded; photo, gallery, camera resume and navigation checked.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo r61 in RAM: 480 × 640 video decoded, eight front/rear switches passed. Test media deleted; recorded audio not listened to.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo: preview measured around 16 fps versus 12 in the previous test. 30 fps is not reached yet.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: automatic NITZ time, persistent setting and guarded manual writes. Sync depends on carrier time; no NTP fallback, timezone selected manually.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Android: assets staged and Java compiled, without a new APK or Android video test at this stage. Mozilla account/Sync, remote location, Firefox OS add-ons and USB file transfer remain unimplemented. No flashing or publication.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] + } + }, + { + "id": "development-2026-10-12-settings-calls-hotspot", + "text": { + "fr": "## En développement — 12 octobre 2026 — Appels et partage Wi-Fi\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Dialogues Gaia : fermeture après ouverture, transitions interrompues et nouvelle saisie après validation refusée.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : délai des services opérateur augmenté ; réglages lents séparés des commandes voix/SMS.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : double appel, présentation du numéro et quatre règles de renvoi raccordés à oFono, avec validation et relecture.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : point d’accès Wi-Fi WPA2 ou ouvert, configuration NetworkManager et pare-feu adapté. Mot de passe privé, accessible uniquement à Paramètres.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) r59 en RAM : 69 panneaux, dialogues et APN vérifiés sur PC/Pixel. Point d’accès enregistré, activé puis arrêté depuis Gaia ; Wi-Fi précédent restauré. 44 tests Python et régressions des dialogues réussis.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Transit Internet vers un autre appareil non testé. Messagerie vocale, blocage d’appels, PIN/PUK, certificats Wi-Fi et partage USB restent ouverts. Ressources Android synchronisées ; aucun nouvel APK, flash ou publication.\n", + "en": "## In development — 12 October 2026 — Calls and Wi-Fi sharing\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Gaia dialogs: close after opening, complete interrupted transitions and allow retry after rejected validation.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: longer carrier-service timeout; slow settings separated from voice/SMS commands.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: call waiting, caller ID and four forwarding rules connected to oFono, with validation and readback.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: WPA2 or open Wi-Fi hotspot, NetworkManager configuration and compatible firewall. Private password, accessible only to Settings.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) r59 in RAM: 69 panels, dialogs and APNs checked on desktop/Pixel. Hotspot saved, started and stopped through Gaia; previous Wi-Fi restored. 44 Python tests and dialog regressions passed.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Internet forwarding to another device untested. Voicemail, call barring, PIN/PUK, Wi-Fi certificates and USB tethering remain open. Android assets synchronized; no new APK, flashing or publication.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] + } + }, + { + "id": "development-2026-10-12-settings-network", + "text": { + "fr": "## En développement — 12 octobre 2026 — Paramètres réseau\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Wi-Fi : adresse IPv4 et débit réels ; ne plus afficher « undefined » lorsqu’une mesure manque.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : choix GSM/UMTS/LTE et modification des APN Internet/MMS existants, avec validation et relecture.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) APN : refuser les changements d’un profil actif, protéger IMS et les mots de passe, tenter la restauration après échec et signaler son résultat.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo : file bornée pour les réglages lents. Lecture des appels en 20 ms pendant une attente de réglages de 5 secondes.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) 38 tests Python, 69 panneaux PC et parcours APN/réseau sur Pixel en RAM vérifiés. Ressources Android synchronisées.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Ajout, suppression et réinitialisation des APN indisponibles. Sélection manuelle d’opérateur et certificats encore ouverts. Aucun nouvel APK, flash ou publication.\n", + "en": "## In development — 12 October 2026 — Network settings\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Wi-Fi: actual IPv4 address and link speed; no “undefined” label when a measurement is missing.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: GSM/UMTS/LTE selection and editing of existing Internet/MMS APNs, with validation and readback.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) APNs: reject changes to active profiles, protect IMS and passwords, attempt rollback after failure and report its result.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo: bounded queue for slow settings. Call status read in 20 ms during a five-second settings wait.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) 38 Python tests, 69 desktop panels and APN/network workflows on RAM-booted Pixel checked. Android assets synchronized.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) APN creation, deletion and reset unavailable. Manual carrier selection and certificates still open. No new APK, flashing or publication.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] + } + }, + { + "id": "development-2026-10-12-settings-audit", + "text": { + "fr": "## En développement — 12 octobre 2026 — Audit des paramètres\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Paramètres : navigation récupérable après échec d’un contrôleur ou d’un dialogue ; génération cohérente des modules.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Filtres d’accessibilité et assistance Vulpes. Panneaux d’origine conservés avec explication des fonctions indisponibles.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) PC : statistiques du profil et téléchargements Gecko. Android : statistiques du stockage privé.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : données mobiles, itinérance, horloge et Wi-Fi masqué raccordés. Erreurs des anciennes interfaces réseau du panneau Son corrigées.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) 97 vues inventoriées ; 69 panneaux PC/Pixel ouverts et sept dialogues natifs vérifiés. Stockage, données et fuseau relus après écriture sur r51 en RAM.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Les vues contextuelles et services indisponibles ne sont pas tous qualifiés. Java Android compilé, sans nouvel APK. Aucun flash, ticket fermé ni publication.\n", + "en": "## In development — 12 October 2026 — Settings audit\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Settings: recover navigation after a controller or dialog fails; consistent module generation.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Accessibility filters and Vulpes support. Original panels retained with explanations for unavailable features.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Desktop: profile statistics and Gecko downloads. Android: private storage statistics.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: mobile data, roaming, clock and hidden Wi-Fi connected. Legacy network errors in Sound settings fixed.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) 97 views inventoried; 69 desktop/Pixel panels opened and seven native dialogs checked. Storage, data and timezone read back after writes on r51 in RAM.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Contextual views and unavailable services are not all qualified. Android Java compiled, without a new APK. No flashing, ticket closure or publication.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] + } + }, + { + "id": "development-2026-10-12-settings-navigation", + "text": { + "fr": "## En développement — 12 octobre 2026 — Navigation des paramètres\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Listes déroulantes : sélecteur Gaia avec confirmation/annulation ; choix Clair/Sombre accessible. Gestionnaire SIM adapté à un emplacement.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Panneaux indisponibles : explication et commandes désactivées au lieu de liens sans effet.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : réseaux Wi-Fi mémorisés séparés des scans lents ; cache conservé lors des lectures d’état.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : informations SIM, centre SMS et accusés de réception reliés au modem.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) r45 en RAM : huit contrôles d’apparence, cinq panneaux et changement réel des accusés SMS validés, puis réglage restauré. Sélecteur PC vérifié ; ressources APK préparées.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Certificats Enterprise et autres options opérateur encore indisponibles. Aucun SMS émis, nouvel APK, flash ou publication.\n", + "en": "## In development — 12 October 2026 — Settings navigation\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Dropdowns: Gaia selector with confirm/cancel; Light/Dark choice accessible. SIM manager adapted to one slot.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Unavailable panels: explanations and disabled controls instead of unresponsive links.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: saved Wi-Fi networks separated from slow scans; cache preserved during status reads.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: SIM information, SMS centre and delivery reports connected to the modem.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) r45 in RAM: eight appearance checks, five panels and actual SMS report setting changes passed, then setting restored. Desktop selector checked; APK assets staged.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Enterprise certificates and other carrier options still unavailable. No SMS sent, new APK, flashing or publication.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] + } + }, + { + "id": "development-2026-10-12-sargo-appearance", + "text": { + "fr": "## En développement — 12 octobre 2026 — Correctifs communs sur Pixel 3a\n\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo : boot r39 en RAM avec correctifs partagés et premier mode sombre. Noyau et configuration matérielle du r35 conservés.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Démarrage, taille d’écran, réglages persistants, batterie, interface d’appel et Fira vérifiés. Services modem, audio, Wi-Fi et Bluetooth actifs.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sept contrôles d’apparence réussis dans Gaia : thème propagé, en-têtes lisibles, libellés placés et retour au clair. Capture Pixel inspectée.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Outil de mise à jour Gaia dans un boot privé, avec empreintes, sources conservées et vérification des composants matériels inchangés.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Aucun nouvel appel, SMS ou appairage testé. Thème complet et comportement Android encore à qualifier. Aucun nouvel APK, flash ou publication.\n", + "en": "## In development — 12 October 2026 — Shared fixes on Pixel 3a\n\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo: r39 RAM boot with shared fixes and initial dark mode. r35 kernel and hardware configuration preserved.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Boot, viewport size, persistent settings, battery, call UI and Fira checked. Modem, audio, Wi-Fi and Bluetooth services active.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Seven Gaia appearance checks passed: propagated theme, readable headers, aligned labels and return to light mode. Pixel screenshot inspected.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Tool to update Gaia in a private boot image, with hashes, retained sources and checks for unchanged hardware components.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) No new call, SMS or pairing tested. Full theme and Android behavior still need qualification. No new APK, flashing or publication.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] + } + }, + { + "id": "development-2026-10-11-appearance-input", + "text": { + "fr": "## En développement — 11 octobre 2026 — Apparence et clavier partagé\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Apparence : Clair, Sombre ou Selon le système, partagé entre les applications et conservé au redémarrage. Fonds d’écran, photos et sites externes préservés.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Clavier : opérations d’édition mutualisées, sélection et effacement, protection contre les réponses tardives.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : neuf contrôles d’apparence et gestes de saisie dans Messages réussis, sans envoi de SMS. Revue visuelle de Paramètres, Messages, Téléphone et Contacts.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Android : option expérimentale de clavier Gaia ; clavier Android conservé par défaut et mode de saisie restauré à la fermeture.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android : réessayer brièvement le même capteur encore occupé, sans réessayer après refus de permission ou fermeture de Photo.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Preview 19 construit localement : signature, alignement, parité et tests du pont/caméra réussis.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Clavier Gaia, caméra avant Android et thème complet encore à qualifier. Sources prêtes pour Sargo, resté sur r35 à cette étape. Aucun upload, flash ni ticket fermé.\n", + "en": "## In development — 11 October 2026 — Appearance and shared keyboard\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Appearance: Light, Dark or Follow system, shared between apps and retained across restarts. Wallpapers, photos and external websites preserved.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Keyboard: shared editing operations, selection and deletion, protection against late replies.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: nine appearance checks and Messages input gestures passed, without sending SMS. Visual review of Settings, Messages, Phone and Contacts.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Android: experimental Gaia keyboard option; Android keyboard remains the default, input mode restored on close.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android: briefly retry the same busy sensor, without retrying after permission denial or closing Camera.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Preview 19 built locally: signature, alignment, parity and bridge/camera tests passed.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Gaia keyboard, Android front camera and full theme still need qualification. Sources ready for Sargo, which remained on r35 at this stage. No upload, flashing or ticket closure.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] + } + }, + { + "id": "development-2026-10-11-settings", + "text": { + "fr": "## En développement — 11 octobre 2026 — Paramètres et accueil\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Paramètres : gestion des sonneries, saisie/effacement du code PIN, désactivation du verrouillage et liste des permissions des applications tierces.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Accueil : première initialisation du second lanceur et aperçus des fonds d’écran.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Langues : actualiser les libellés Vulpes au changement de langue ; anglais de secours hors français. Fermer le panneau d’échec caméra et les bandeaux d’installation.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Android : ouvrir le volet Gaia en touchant la barre de statut. Preview 18 construit, signé et comparé aux sources.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : neuf parcours Gaia, cinq contrôles de bandeaux et douze tests activités/caméra réussis. Bluetooth Sargo r35 confirmé par l’utilisateur.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Redémarrage à chaud du compositeur encore instable : fichiers restaurés et retour à r35. Corrections non qualifiées sur Sargo à cette étape ; clavier/thème Android ouverts. Aucun APK publié ni flash.\n", + "en": "## In development — 11 October 2026 — Settings and homescreens\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Settings: ringtone management, PIN entry/deletion, lock disabling and third-party app permission list.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Home: first initialization of the second launcher and wallpaper previews.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Languages: refresh Vulpes labels after language changes; English fallback outside French. Dismiss camera failure panels and installation banners.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Android: open the Gaia tray by tapping the status bar. Preview 18 built, signed and compared with sources.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: nine Gaia workflows, five banner checks and twelve activity/camera tests passed. Sargo r35 Bluetooth confirmed by the user.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Compositor hot restart still unstable: files restored and return to r35. Fixes not yet qualified on Sargo at this stage; Android keyboard/theme open. No APK publication or flashing.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] + } + }, + { + "id": "development-2026-10-09-mms-bluetooth", + "text": { + "fr": "## En développement — 9 octobre 2026 — MMS et Bluetooth\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Bluetooth : adaptateur Gaia partagé, commandes matérielles séparées et accès limité aux applications autorisées.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : activation, découverte, appairage explicite, connexion et suppression depuis le panneau Bluetooth Gaia. Modules audio Bluetooth intégrés.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : services réseau Qualcomm et transport MMS raccordés. Une déconnexion D-Bus ne termine plus le daemon MMS. État persistant, reprise sans doublon ni renvoi automatique incertain.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) r35 en RAM : MMS avec image visible et accepté par le MMSC Orange ; réception destinataire à confirmer. Cinq appareils Bluetooth détectés, A2DP vers PC validé.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) 191 tests Python Tundra et 18 tests JavaScript réussis. Ramdisk allégé et ressources Android synchronisées.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Audio d’appel via casque non qualifié. Aucun nouvel APK, flash ni push à cette étape.\n", + "en": "## In development — 9 October 2026 — MMS and Bluetooth\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Bluetooth: shared Gaia adapter, separate hardware commands and access restricted to authorized apps.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: power, discovery, explicit pairing, connection and removal from Gaia Bluetooth settings. Bluetooth audio modules included.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: Qualcomm network services and MMS transport connected. D-Bus disconnect no longer terminates the MMS daemon. Persistent state, retry without duplicates or uncertain automatic resends.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) r35 in RAM: image MMS visible and accepted by Orange MMSC; recipient delivery still unconfirmed. Five Bluetooth devices discovered, A2DP to desktop verified.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) 191 Tundra Python tests and 18 JavaScript tests passed. Smaller ramdisk and synchronized Android assets.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Headset call audio unqualified. No new APK, flashing or push at this stage.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] + } + }, + { + "id": "development-2026-10-08-camera-mms", + "text": { + "fr": "## En développement — 8 octobre 2026 — Caméra et transport MMS\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Caméra : arrêter le flux avant de détacher la vidéo ; attendre la fermeture du capteur, réinitialiser le viseur et recalculer sa taille après rotation.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) MMS : préserver les pièces jointes Blob et garder les messages en attente jusqu’à confirmation du transport.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo : viseur WebGL réutilisé sans recapture vidéo intermédiaire ; écran maintenu éveillé pendant l’aperçu.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC et Sargo : parcours Photo/Galerie et huit bascules natives vérifiés. JPEG, focus et zoom fonctionnels ; arrière ~16 FPS, avant ~25–27 FPS dans cet essai.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : file MMS persistante, codec Nuntium et transport oFono. Interface texte/photo testée avec transport simulé.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Données mobiles bloquantes pour les MMS opérateur à cette étape. Ressources Android synchronisées ; aucun nouvel APK, flash ou push.\n", + "en": "## In development — 8 October 2026 — Camera and MMS transport\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Camera: stop the stream before detaching video; await sensor closure, reset the viewfinder and recalculate size after rotation.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) MMS: preserve Blob attachments and keep messages pending until transport confirmation.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo: reused WebGL viewfinder without intermediate video capture; screen stays awake during preview.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop and Sargo: Camera/Gallery workflows and eight native switches checked. JPEG, focus and zoom working; rear ~16 fps, front ~25–27 fps in this test.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: persistent MMS queue, Nuntium codec and oFono transport. Text/photo UI tested with simulated transport.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Mobile data blocked carrier MMS at this stage. Android assets synchronized; no new APK, flashing or push.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] + } + }, + { + "id": "development-2026-10-08-call-log-dialogs", + "text": { + "fr": "## 8 octobre 2026 — Rappel et messages d’erreur\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Dialogues Gaia : texte visible au lieu d’un écran gris avec un bouton seul.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Journal d’appel : rappel via le même service que le pavé numérique, sans anciennes API Gecko. Fichier partagé PC/Android/Sargo.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) MMS : signaler clairement l’envoi indisponible, garder texte et pièces jointes, ne plus annoncer un faux mode avion.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : dialogues FR/EN et composeur préservé. Sargo en RAM : 11 contrôles Gaia réussis, rappel testé avec modem simulé.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Aucun appel réel ni validation audio dans cette étape. Ressources Android synchronisées, aucun nouvel APK publié.\n", + "en": "## 8 October 2026 — Call-log redial and error dialogs\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Gaia dialogs: visible text instead of a grey screen with a lone button.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Call log: redial through the same service as the keypad, without legacy Gecko APIs. Shared desktop/Android/Sargo file.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) MMS: clearly report unavailable sending, retain text and attachments, no false airplane-mode message.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: FR/EN dialogs and preserved composer. RAM-booted Sargo: 11 Gaia checks passed, redial tested with a simulated modem.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) No real call or audio validation at this stage. Android assets synchronized, no new APK published.\n" + }, + "metadata": { + "kind": "history", + "status": "history", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] + } + }, + { + "id": "development-2026-10-08-sms-audio", + "text": { + "fr": "## En développement — 8 octobre 2026 — SMS et audio\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS : envoyer en un appui en conservant le clavier. Trombone raccordé au menu Gaia : image, audio, vidéo ou contact.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Interface : indicateur de volume visible, gestes du verrouillage et balayage des notifications corrigés.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : clavier, pièces jointes, Galerie, interface d’appel et volume vérifiés ; notification balayée sans déplacer la page. Aucun nouvel APK publié.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : accès à PulseAudio, relais Qualcomm, protocole D-Bus et périphériques de calibration rétablis. Service audio Android concurrent retiré.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : 11 contrôles Gaia, calibration et prévisualisations arrière/avant/arrière réussis.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Appels échouant avant sonnerie avec une erreur réseau dans cet essai. Audio bidirectionnel, geste Pixel et MMS encore non validés. Aucun flash ni publication.\n", + "en": "## In development — 8 October 2026 — SMS and audio\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS: send in one tap while keeping the keyboard. Paperclip connected to the Gaia image, audio, video or contact menu.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) UI: visible volume indicator, lockscreen gestures and notification swiping fixed.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: keyboard, attachments, Gallery, call UI and volume checked; notification swiped without moving the page. No new APK published.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: PulseAudio access, Qualcomm relay, D-Bus protocol and calibration devices restored. Conflicting Android audio service removed.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: 11 Gaia checks, calibration and rear/front/rear previews passed.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Calls failed before ringing with a network error in this test. Two-way audio, Pixel gesture and MMS still unverified. No flashing or publication.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] + } + }, + { + "id": "development-2026-10-08-radio-reliability", + "text": { + "fr": "## En développement — 8 octobre 2026 — Fiabilité téléphonie\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : service radio maintenu après fermeture d’un client. Jamais de nouvel appel ou SMS automatique après une réponse perdue. Erreurs sans numéro ni texte du message.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Diagnostic réseau/audio sans identifiants personnels et paramètres opérateur épinglés dans l’image native.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS : attendre le vrai gestionnaire Gaia et accepter les identifiants oFono avec majuscules. Contrôle des conversations rétabli sans réémission.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : LTE retrouvé après redémarrage, SMS confirmé envoyé par le modem, microphone non silencieux et lecture audio acceptée.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Réception destinataire, audio d’appel bidirectionnel et stabilité modem restent à qualifier. Compositeur instable au redémarrage à chaud. Aucun nouvel APK, flash ou push.\n", + "en": "## In development — 8 October 2026 — Telephony reliability\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: radio service survives client closure. No automatic new call or SMS after a lost response. Errors exclude phone numbers and message text.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Network/audio diagnostics without personal identifiers, and pinned carrier settings in the native image.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS: wait for the actual Gaia manager and accept uppercase oFono IDs. Conversation checks restored without resending.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: LTE restored after reboot, SMS confirmed sent by the modem, non-silent microphone and audio playback accepted.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Recipient delivery, two-way call audio and modem stability remain unqualified. Compositor hot restart unstable. No new APK, flashing or push.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] + } + }, { "id": "2026-10-08-source-publication", "text": { - "fr": "## 8 octobre 2026 — Mise à jour des sources publiques\n\n### Commun · PC / Android / Tundra\n\n- Regrouper les correctifs récents de caméra, Contacts, SMS, Marketplace et les outils de construction et de restauration Sargo dans les sources publiques. Mettre à jour les README français et anglais avec les capacités et limites vérifiées.\n- Corriger le contrôle de parité Android pour créer son dossier de rapports sur une copie neuve du dépôt. Vérifier que la configuration Wi-Fi native embarque un répertoire de connexions vide.\n- Conserver les tickets, feuilles de route, documents internes, données personnelles et clés hors du dépôt.\n\n### Paquets et qualification\n\n- L’APK public reste Preview17 : les correctifs du 8 octobre nécessitent une prochaine compilation. Aucune nouvelle image native publiée ni flash dans cette étape.\n- Sur Sargo, l’enregistrement cellulaire reste en recherche lors du dernier contrôle ; audio bidirectionnel, appairage Bluetooth et profils restent à qualifier.", - "en": "## 8 October 2026 — Public source update\n\n### Shared · Desktop / Android / Tundra\n\n- Bring recent camera, Contacts, SMS and Marketplace fixes, plus Sargo build and recovery tools, into the public sources. Update both READMEs with verified capabilities and limitations.\n- Make the Android parity check create its report directory in a fresh checkout. Verify that native Wi-Fi configuration ships an empty connections directory.\n- Keep tickets, roadmaps, internal documents, personal data and keys outside the repository.\n\n### Packages and qualification\n\n- The public APK remains Preview17; the 8 October fixes require a future build. No new native image or flash in this step.\n- At the latest Sargo check, cellular registration is still searching; two-way call audio, Bluetooth pairing and profiles remain to be qualified." + "fr": "## 8 octobre 2026 — Mise à jour des sources publiques\n\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Publication des correctifs Caméra, Contacts, SMS et Marketplace, et des outils Sargo de construction et de restauration.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) README français et anglais actualisés avec les fonctions et limites vérifiées.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Création du dossier de rapports lors du contrôle de parité Android sur un dépôt neuf.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Configuration Wi-Fi native vérifiée : aucun profil de connexion embarqué.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Documents internes, données personnelles et clés exclus du dépôt.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) APK public inchangé (Preview17) ; correctifs du 8 octobre non inclus. Aucune nouvelle image native publiée.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo : modem encore en recherche réseau ; audio des appels, appairage et profils Bluetooth à valider.\n", + "en": "## 8 October 2026 — Public source update\n\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Published Camera, Contacts, SMS and Marketplace fixes, plus Sargo build and recovery tools.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Updated French and English READMEs with verified features and limitations.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Create the report directory when checking Android asset parity in a fresh checkout.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Checked native Wi-Fi configuration: no bundled connection profiles.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Internal documents, personal data and keys excluded from the repository.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Public APK unchanged (Preview17); October 8 fixes not included. No new native image published.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo: modem still searching for a network; call audio, Bluetooth pairing and profiles await validation.\n" + }, + "metadata": { + "kind": "history", + "status": "history", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "development-2026-10-08-camera-lifecycle", "text": { - "fr": "## 8 octobre 2026 — Caméra et parcours Contacts · en développement\n\n### Commun · Gaia / PC / Android / Tundra\n\n- Sérialiser les ouvertures de caméra, attendre la fermeture du capteur précédent et libérer les pistes et ressources vidéo même après un échec. Stabiliser l’identification avant/arrière sur PC et Android.\n- Conserver les images binaires lors des échanges PC/Tundra ; encoder explicitement les photos et dates des contacts pour le pont JSON Android.\n- Corriger l’actualisation de la fiche après modification d’un contact. Rétablir les activités Contacts → SMS, sélection d’un numéro et ouverture du pavé numérique, en préservant les demandes imbriquées et les points d’entrée de Gaia.\n- Réouvrir Galerie en mode sélection pour ajouter une photo à un contact, même lorsque Galerie est déjà ouverte en consultation. Sélection, recadrage et enregistrement de la photo validés sur PC.\n- Ajouter des tests de caméra, de stockage des contacts, de permissions, de transport binaire et de routage des activités.\n\n### PC\n\n- Vérifier le parcours original Appareil photo → capture JPEG → Galerie → ouverture de la photo → retour caméra.\n- Vérifier dans Gaia : création et modification d’un contact, actualisation de la fiche, recherche sans accents et sans résultats, favoris, destinataire SMS, sélection d’un numéro, pavé numérique prérempli, annulation puis confirmation de suppression. Le test n’émet ni appel ni SMS et supprime son contact temporaire.\n\n### Tundra · Sargo\n\n- Vérifier en RAM sur Pixel 3a : autofocus, zoom, quatre alternances avant/arrière avec nouvelles trames, capture JPEG depuis Gaia, stockage, ouverture dans Galerie et arrêt du capteur à l’accueil.\n- Vérifier les conversations déjà stockées, le clavier et les notifications. Recherche Wi-Fi : quatre réseaux détectés. Nouvelle émission/réception SMS et audio des appels encore en qualification.\n- Ajouter les paquets BlueZ et les bibliothèques nécessaires, verrouillés par SHA256. Le contrôleur Bluetooth démarre, s’active et lance une recherche ; l’appairage depuis Gaia et les profils ne sont pas encore validés.\n- Ajouter le magasin de certificats natif : HTTPS vérifié sans désactivation de TLS. Tester les archives altérées, collisions de fichiers et reconstructions répétées.\n\n- Corriger les fichiers Contacts embarqués dans le build de démarrage : la création, l’édition, le passage vers SMS, le sélecteur et le pavé numérique passent aussi avec les gestes tactiles sur Sargo.\n\n### Android · APK\n\n- Publier l’APK Preview17 existant, avec téléchargement et SHA256 vérifiés. Il précède les correctifs du 8 octobre.\n- Corriger la transmission des destinataires et du texte de Gaia vers l’application SMS Android, y compris la réponse à une conversation et le numéro saisi sans validation préalable. Tests du pont réussis ; essai Android réel encore nécessaire.\n- Réempaqueter les ressources communes corrigées pour la prochaine compilation ; aucun nouvel APK incluant ces correctifs n’est publié.\n\n**État :** builds natifs de qualification démarrés en RAM. Aucun flash, push de code ni publication de documentation privée dans cette étape. La qualification complète des fonctions de téléphone reste ouverte.\n", - "en": "## 8 October 2026 — Camera and Contacts workflows · in development\n\n### Shared · Gaia / Desktop / Android / Tundra\n\n- Serialize camera opening, await the previous sensor shutdown and release tracks and video resources after failures. Keep stable front/rear identities on desktop and Android.\n- Preserve binary images across desktop/Tundra transport; explicitly encode contact photos and dates for Android's JSON bridge.\n- Refresh contact details after editing. Restore Contacts → SMS, number picking and prefilled dialer activities, preserving nested requests and Gaia entry points.\n- Reopen Gallery in picker mode when adding a contact photo, even when Gallery is already open for browsing. Selection, cropping and photo persistence verified on desktop.\n- Add camera lifecycle, contact persistence, permissions, binary transport and activity routing tests.\n\n### Desktop\n\n- Verify the original Camera → JPEG capture → Gallery → photo opening → camera workflow.\n- Verify Gaia contact creation, editing, detail refresh, accent-insensitive search and empty results, favourites, SMS recipient, number picker, prefilled keypad, cancelled deletion and confirmed deletion. The test sends no message and makes no call; its temporary contact is removed.\n\n### Tundra · Sargo\n\n- Verify on a RAM-booted Pixel 3a: autofocus, zoom, four front/rear switches with fresh frames, shutter capture, JPEG persistence, Gallery opening and sensor shutdown on Home.\n- Check stored conversations, keyboard and notifications. Wi-Fi scan finds four networks. New SMS transmission/reception and call audio are still being qualified.\n- Add SHA256-pinned BlueZ packages and required libraries. The controller starts, powers on and discovers; pairing through Gaia and Bluetooth profiles are not yet validated.\n- Add native trusted certificates and verify HTTPS without disabling TLS. Test corrupted archives, file collisions and repeatable staging.\n\n- Include the corrected Contacts files in the boot build: contact creation, editing, SMS handoff, picking and dialer prefilling also pass using touch gestures on Sargo.\n\n### Android · APK\n\n- Publish the existing Preview17 APK and verify its download and SHA256. It predates the 8 October fixes.\n- Fix recipient and body handoff from Gaia to the Android SMS app, including replies and a typed recipient that was not yet committed. Bridge tests pass; real Android validation is still required.\n- Repackage corrected shared resources for the next build; no APK containing these new fixes has been published.\n\n**Status:** native qualification builds booted in RAM. No flashing, source push or private documentation publication in this step. Full phone qualification remains open.\n" + "fr": "## 8 octobre 2026 — Caméra et parcours Contacts · en développement\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Caméra : ouvertures successives, libération du capteur et identification avant/arrière corrigées.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Contacts : transport des photos et des dates corrigé sur PC, Android et Tundra.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Contacts : actualisation après modification, ouverture de SMS, sélection d’un numéro et préremplissage du pavé numérique rétablis.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Galerie : ouverture en mode sélection pour la photo d’un contact, même si l’application est déjà ouverte.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : fichiers Contacts corrigés inclus dans l’image de démarrage.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android : transmission du destinataire et du texte à l’application SMS, y compris les réponses et les numéros non encore validés.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Tests de caméra, stockage des contacts, permissions, transport binaire et activités.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : capture JPEG, Galerie, photo de contact, création, édition, recherche, favoris et suppression vérifiés. Aucun appel ni SMS émis.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo en RAM : autofocus, zoom, quatre bascules de caméra, capture et Galerie vérifiés ; parcours Contacts testés au toucher.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : conversations stockées, clavier et notifications vérifiés ; quatre réseaux Wi-Fi détectés. Nouveaux SMS et audio d’appel à valider.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : BlueZ et ses dépendances avec empreintes SHA256. Activation et recherche Bluetooth testées ; appairage et profils à valider.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : magasin de certificats natif. HTTPS, archives altérées, collisions de fichiers et reconstructions testés.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Preview17 publié avec téléchargement et SHA256 vérifiés. Les correctifs ci-dessus restent dans les sources ; essai SMS Android réel nécessaire. Aucun flash Sargo.\n", + "en": "## 8 October 2026 — Camera and Contacts workflows · in development\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Camera: corrected sequential opening, sensor cleanup and front/rear identification.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Contacts: corrected photo and date transport on desktop, Android and Tundra.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Contacts: restored refresh after editing, SMS handoff, number picking and dialer prefilling.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Gallery: open the contact photo picker even when the app is already running.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: include corrected Contacts files in the boot image.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android: pass recipients and text to the SMS app, including replies and unconfirmed typed numbers.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Tests for camera lifecycle, contact storage, permissions, binary transport and activities.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: verified JPEG capture, Gallery, contact photos, creation, editing, search, favourites and deletion. No calls or SMS sent.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo in RAM: verified autofocus, zoom, four camera switches, capture and Gallery; tested Contacts with touch input.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: checked stored conversations, keyboard and notifications; detected four Wi-Fi networks. New SMS and call audio await validation.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: BlueZ and dependencies pinned by SHA256. Power-on and discovery tested; pairing and profiles await validation.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: native certificate store. Tested HTTPS, corrupted archives, file collisions and repeated builds.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Published Preview17 with download and SHA256 checks. These fixes remain in source; Android SMS needs a real-device test. No Sargo flash.\n" + }, + "metadata": { + "kind": "history", + "status": "history", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "development-2026-10-06-marketplace-theme", "text": { - "fr": "## 6 octobre 2026 — Thème Marketplace intégré · en développement\n\n### Commun · Gaia / PC / Android / Tundra\n\n- Ouvrir le catalogue avec `mode=app`, conservé dans les liens, la recherche, les filtres, les fiches et le changement de langue.\n- Préparer un thème web Vulpes dédié : Fira locale, navigation compacte, boutons tactiles, cartes lisibles, filtres repliables et captures défilantes. Retirer les en-têtes du site dans le mode intégré, tout en conservant l’interface du site classique.\n- Conserver les confirmations et messages système côté Gaia. Le thème ne donne aucun privilège supplémentaire aux pages du catalogue.\n- Vérifier 16 pages FR/EN, 2 pages en mode classique et les ressources du thème. Rendus mobiles de 320 et 360 pixels inspectés. Lanceurs PC et Sargo mis à jour ; les sources seront reprises au prochain empaquetage Android.\n\nFichiers du site prêts localement, non déployés sur le serveur public. Aucun push ni nouvelle publication APK/ROM.\n", - "en": "## 6 October 2026 — Embedded Marketplace theme · in development\n\n### Shared · Gaia / Desktop / Android / Tundra\n\n- Open the catalogue with `mode=app`, retained in navigation, search, filters, app details and language switches.\n- Prepare a dedicated Vulpes web theme with local Fira fonts, compact navigation, touch-friendly controls, readable cards, collapsible filters and scrolling screenshots. Omit website headers in embedded mode while preserving the standard website layout.\n- Keep confirmations and system messages in Gaia. The theme grants catalogue pages no additional privileges.\n- Check 16 French/English pages, 2 standard pages and theme assets. Inspect mobile renders at 320 and 360 pixels. Desktop and Sargo launchers updated; shared sources will be included in the next Android packaging run.\n\nWebsite files prepared locally, not deployed to the public server. No push or new APK/ROM publication.\n" + "fr": "## 6 octobre 2026 — Thème Marketplace intégré · en développement\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Marketplace : thème intégré avec Fira locale, navigation compacte, boutons tactiles, filtres repliables et captures défilantes.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Conservation de `mode=app` dans la navigation, la recherche, les filtres et le changement de langue.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) En-têtes du site masqués en mode intégré. Confirmations gérées par Gaia, sans privilèges supplémentaires pour le catalogue.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) 16 pages FR/EN, 2 pages classiques et affichages mobiles à 320 et 360 pixels vérifiés.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Thème prêt localement, non déployé. Lanceurs PC et Sargo mis à jour ; intégration Android au prochain APK.\n", + "en": "## 6 October 2026 — Embedded Marketplace theme · in development\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Marketplace: embedded theme with local Fira fonts, compact navigation, touch controls, collapsible filters and scrolling screenshots.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Preserve `mode=app` across navigation, search, filters and language changes.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Hide website headers in embedded mode. Gaia handles confirmations; the catalogue receives no extra privileges.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Checked 16 French/English pages, 2 standard pages and mobile layouts at 320 and 360 pixels.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Theme ready locally, not deployed. Desktop and Sargo launchers updated; Android integration pending the next APK.\n" + }, + "metadata": { + "kind": "history", + "status": "history", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "development-2026-10-06-system-banner", "text": { - "fr": "## 6 octobre 2026 — Bandeaux Gaia · en développement\n\n### Commun · Gaia / PC / Android / Tundra\n\n- Faire disparaître le bandeau « Application installée » après son délai normal, même si la fin d’animation ne remonte pas du composant Gaia. Conserver son apparence et permettre sa fermeture au toucher.\n- Annuler les anciens délais lorsqu’un nouveau message remplace le précédent pour éviter une disparition prématurée.\n- Vérifier sur PC l’affichage, l’expiration, le remplacement et la fermeture au clic ; vérifier sur le Pixel le retrait effectif du bandeau après cinq secondes. Correctif déployé sur l’essai Sargo en RAM et disponible dans les sources communes pour le prochain APK.\n\nAucun flash ni publication dans cette étape.\n", - "en": "## 6 October 2026 — Gaia banners · in development\n\n### Shared · Gaia / Desktop / Android / Tundra\n\n- Dismiss the “Application installed” banner after its normal timeout even if the animation-end event does not reach the Gaia component host. Keep its appearance and tap-to-dismiss behavior.\n- Cancel old timers when a new message replaces the previous one, preventing premature dismissal.\n- Check display, expiry, replacement and click dismissal on desktop; verify actual removal after five seconds on the Pixel. Fix deployed to the Sargo RAM trial and available in shared sources for the next APK.\n\nNo flash or publication in this step.\n" + "fr": "## 6 octobre 2026 — Bandeaux Gaia · en développement\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Fermeture automatique du bandeau « Application installée », même sans événement de fin d’animation.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Annulation des anciens délais lorsqu’un nouveau bandeau remplace le précédent.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : affichage, expiration, remplacement et fermeture au clic vérifiés. Sargo : disparition après cinq secondes vérifiée en RAM.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Correctif disponible dans les sources communes pour le prochain APK. Aucun flash ni publication.\n", + "en": "## 6 October 2026 — Gaia banners · in development\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Automatically dismiss the “Application installed” banner even without an animation-end event.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Cancel old timers when a new banner replaces the previous one.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: checked display, expiry, replacement and click dismissal. Sargo: verified dismissal after five seconds in RAM.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Fix available in shared sources for the next APK. No flash or release.\n" + }, + "metadata": { + "kind": "history", + "status": "history", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "development-2026-10-06-wifi", "text": { - "fr": "## 6 octobre 2026 — Wi-Fi et panneaux Paramètres · en développement\n\n### Commun · Gaia / PC / Android / Tundra\n\n- Terminer l’ouverture d’un panneau Paramètres même si Gecko ne produit pas l’événement de fin d’animation. Conserver les transitions Gaia et éviter les doubles initialisations.\n- Correctif dans les sources communes et le serveur PC ; inclus au prochain empaquetage Android. Aucun nouvel APK publié dans cette étape.\n\n### Tundra · Sargo\n\n- Corriger les événements de signal Wi-Fi : transmettre le réseau associé et ne pas annoncer de signal de connexion lorsque le téléphone est déconnecté. L’événement incomplet faisait échouer l’affichage des réseaux dans Gaia.\n- Sur le Pixel, vérifier le pilote, le service natif, le passage par Gecko et l’affichage du panneau corrigé : quatre points d’accès détectés, regroupés en deux réseaux, sans erreur de recherche.\n- Connexion Wi-Fi confirmée par l’utilisateur ; requête HTTPS réelle réussie depuis Gecko vers example.org (HTTP 200, vérification des certificats active). Correctifs appliqués à l’essai en RAM ; aucun flash ni push.\n", - "en": "## 6 October 2026 — Wi-Fi and Settings panels · in development\n\n### Shared · Gaia / Desktop / Android / Tundra\n\n- Complete Settings panel navigation even when Gecko does not emit an animation-end event. Preserve Gaia transitions and prevent duplicate initialization.\n- Updated shared sources and the desktop server; included in the next Android packaging run. No new APK published in this step.\n\n### Tundra · Sargo\n\n- Fix Wi-Fi signal events: include the associated network and do not emit connection information while disconnected. The incomplete event prevented Gaia from displaying scan results.\n- Check the driver, native service, Gecko bridge and corrected panel on the Pixel: four access points detected, grouped into two networks, with no scan error.\n- Wi-Fi connection confirmed by the user; a real HTTPS request from Gecko to example.org passed (HTTP 200, certificate verification enabled). Fixes applied to the RAM trial; no flash or Git push.\n" + "fr": "## 6 octobre 2026 — Wi-Fi et panneaux Paramètres · en développement\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Paramètres : ouverture des panneaux même sans événement de fin d’animation, sans double initialisation.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : événements Wi-Fi corrigés pour rétablir l’affichage des réseaux et signaler correctement la déconnexion.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : quatre points d’accès détectés, regroupés en deux réseaux. Connexion confirmée et requête HTTPS réussie avec certificats vérifiés.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sources communes, serveur PC et essai Sargo en RAM mis à jour. Correctifs non inclus dans l’APK public.\n", + "en": "## 6 October 2026 — Wi-Fi and Settings panels · in development\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Settings: open panels even without an animation-end event, without duplicate initialization.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: corrected Wi-Fi events to restore network results and report disconnection properly.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: detected four access points grouped into two networks. Connection confirmed and HTTPS request passed with certificate verification.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Shared sources, desktop server and Sargo RAM trial updated. Fixes not included in the public APK.\n" + }, + "metadata": { + "kind": "history", + "status": "history", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "development-2026-10-06-marketplace-views", "text": { - "fr": "## 6 octobre 2026 — Marketplace · en développement\n\n### PC / Tundra · Sargo\n\n- Maintenir le catalogue sous la barre de statut et dans les limites de son application. Masquer sa vue web à l’accueil et dans le multitâche ; la retirer à la fermeture de l’application.\n- Éviter qu’une navigation annulée ou l’ancienne page d’erreur masque une nouvelle tentative.\n- Afficher un message local et proposer une nouvelle tentative lorsque le catalogue est inaccessible, au lieu de laisser sa page d’erreur recouvrir Gaia.\n- Sur PC : navigation, erreur réseau, nouvelle tentative, retour à l’accueil et fermeture depuis le multitâche vérifiés. Sept contrôles directs passent sur Sargo en RAM, sans fenêtre de débogage, avec une page locale et une précondition de déverrouillage forcée ; le catalogue distant nécessite une connexion Internet sur le téléphone.\n\n### Android · APK\n\n- Reconstruire l’APK local Preview17 avec le message d’erreur Marketplace commun. Signature, alignement et parité des ressources vérifiés ; nouvelle régression sur appareil Android non effectuée dans cette étape.\n\nAucune publication dans cette étape.\n", - "en": "## 6 October 2026 — Marketplace · in development\n\n### Desktop / Tundra · Sargo\n\n- Keep the catalogue below the status bar and inside its owning application. Hide its web view on Home and in the task switcher; remove it when the app closes.\n- Prevent a cancelled navigation or a previous error page from hiding a retry.\n- Show a local error message with a retry action when the catalogue is unavailable, instead of leaving its error page over Gaia.\n- Desktop navigation, network failure, retry, Home and closing from the task switcher checked. Seven direct checks pass on Sargo in RAM without a debugger window, using a local page and a forced-unlock precondition; the remote catalogue requires an Internet connection on the phone.\n\n### Android · APK\n\n- Rebuild the local Preview17 APK with the shared Marketplace error message. Signature, alignment and asset parity checked; no new Android device regression run in this step.\n\nNothing published in this step.\n" + "fr": "## 6 octobre 2026 — Marketplace · en développement\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) PC/Sargo : catalogue Marketplace contenu sous la barre de statut, masqué à l’accueil et retiré à la fermeture.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Une navigation annulée ou une ancienne page d’erreur ne masque plus la nouvelle tentative.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Message d’erreur local avec bouton Réessayer lorsque le catalogue est inaccessible.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : navigation, erreur réseau, nouvelle tentative, accueil et fermeture vérifiés.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo en RAM : sept contrôles réussis avec une page locale et déverrouillage forcé. Catalogue distant non validé sans connexion Internet.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) APK Preview17 local reconstruit ; signature, alignement et ressources vérifiés. Aucun nouveau test sur appareil Android ni publication.\n", + "en": "## 6 October 2026 — Marketplace · in development\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Desktop/Sargo: keep Marketplace below the status bar, hide it on Home and remove it when closed.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Cancelled navigation or an old error page no longer hides a retry.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Local error message with a Retry button when the catalogue is unavailable.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: verified navigation, network errors, retry, Home and closing.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo in RAM: seven checks passed with a local page and forced unlock. Remote catalogue not validated without Internet access.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Rebuilt local Preview17 APK; checked signature, alignment and assets. No new Android device test or release.\n" + }, + "metadata": { + "kind": "history", + "status": "history", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "2.7-preview.17", "text": { - "fr": "## 2.7-preview.17 — APK construit · 5 octobre 2026\n\n### Commun · Gaia / PC\n\n- Remettre Marketplace sur l’accueil avec son icône originale et le catalogue Vulpes. Ouvrir sa version française pour les langues fr, anglaise sinon.\n- Adapter les téléchargements du catalogue en installations confirmées : vérifier les archives ZIP, ajouter l’application à l’accueil et permettre son lancement et sa désinstallation.\n- Conserver les applications installées séparément des sources Gaia, sur des origines distinctes et sans privilèges système hérités des anciens manifestes.\n- Valider sur PC le parcours réel avec 2048 : catalogue, confirmation, installation, lancement, persistance et désinstallation. Contrôler les archives corrompues, chemins invalides, limites, annulation et confirmations à usage unique.\n\n### Android · APK\n\n- Construire et signer Preview17 avec le même Marketplace et le même service d’installation. Utiliser la vue GeckoView existante pour le catalogue et le stockage privé Android pour les paquets.\n- Vérifier sur émulateur Android 16/API36 le clic Installer, la confirmation, l’apparition sur l’accueil et le lancement de 2048 sans accès au pont système Vulpes. Vérifier aussi la conservation après arrêt forcé et relance, puis la désinstallation.\n\n### Tundra · Sargo\n\n- Préparer une image de boot locale incluant Marketplace, l’installateur et le serveur des applications ajoutées. Démarrage en RAM effectué sur le Pixel le 6 octobre : accueil et ressources Marketplace vérifiés, sans flash. Neuf contrôles système sur dix passent ; le contrôle global Gaia relève une variation du viewport au déverrouillage. Installation de paquets sur Sargo encore à qualifier.\n\n**Limites :** compatibilité initiale avec les applications web empaquetées. Les anciennes API privilégiées, applications hébergées et services disparus ne sont pas restaurés automatiquement. Rien publié sur Forgejo ni sur le site.\n", - "en": "## 2.7-preview.17 — APK built · 5 October 2026\n\n### Shared · Gaia / Desktop\n\n- Restore Marketplace on Home with its original icon and the Vulpes catalogue. Select French for fr languages and English otherwise.\n- Turn catalogue downloads into confirmed installations: validate ZIP archives, add the application to Home and support launch and removal.\n- Store installed applications separately from Gaia sources, on distinct origins and without system privileges inherited from archived manifests.\n- Validate the real desktop workflow with 2048: catalogue, confirmation, installation, launch, persistence and removal. Check damaged archives, invalid paths, limits, cancellation and single-use confirmations.\n\n### Android · APK\n\n- Build and sign Preview17 with the same Marketplace and installation service. Use the existing GeckoView browser for the catalogue and private Android storage for packages.\n- Verify the Install click, confirmation, Home icon and 2048 launch without access to the Vulpes system bridge on an Android 16/API36 emulator. Also verify persistence after force-stop/relaunch and removal.\n\n### Tundra · Sargo\n\n- Prepare a local boot image containing Marketplace, the installer and the added-application server. RAM boot performed on the Pixel on 6 October: Home and Marketplace resources verified, without flashing. Nine of ten system checks pass; the overall Gaia check records a viewport change during unlocking. Package installation on Sargo still needs qualification.\n\n**Limitations:** initial support for packaged web applications. Old privileged APIs, hosted apps and discontinued services are not restored automatically. Nothing published to Forgejo or the website.\n" + "fr": "## 2.7-preview.17 — APK construit · 5 octobre 2026\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Marketplace sur l’accueil, avec son icône originale et le catalogue Vulpes en français ou en anglais.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Installation confirmée des applications ZIP, lancement depuis l’accueil et désinstallation.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Applications installées isolées des sources Gaia et des privilèges système.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : parcours complet avec 2048 et contrôles des archives, chemins, limites, annulations et confirmations à usage unique.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) APK Preview17 construit et signé avec Marketplace, GeckoView et stockage privé des paquets.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Émulateur Android 16/API 36 : installation, lancement, isolation, conservation après redémarrage de l’application et désinstallation de 2048.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo en RAM le 6 octobre : accueil et ressources Marketplace vérifiés, 9 contrôles système sur 10 réussis. Variation du viewport au déverrouillage ; installation de paquets à valider.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Prise en charge initiale des applications web empaquetées. Anciennes API privilégiées et applications hébergées non restaurées. Aucune publication à cette étape.\n", + "en": "## 2.7-preview.17 — APK built · 5 October 2026\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Marketplace on Home with its original icon and the Vulpes catalogue in French or English.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Confirmed ZIP app installation, launch from Home and removal.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Installed apps isolated from Gaia sources and system privileges.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: full 2048 workflow plus archive, path, limit, cancellation and single-use confirmation checks.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Preview17 APK built and signed with Marketplace, GeckoView and private package storage.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Android 16/API 36 emulator: 2048 installation, launch, isolation, persistence after app restart and removal.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo in RAM on October 6: Home and Marketplace assets verified, 9 of 10 system checks passed. Viewport changes on unlock; package installation awaits validation.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Initial support for packaged web apps. Legacy privileged APIs and hosted apps not restored. No release at this stage.\n" + }, + "metadata": { + "kind": "version", + "status": "built", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "2.7-preview.16", "text": { - "fr": "## 2.7-preview.16 — APK construit · 5 octobre 2026\n\n### Commun · Gaia / PC\n\n- Rétablir les sélecteurs Gaia de fond d’écran et de sons, leur retour vers Paramètres et leur annulation par Accueil. Partager le transport des images et sons enregistrés entre PC et Android.\n- Propager les changements de langue à l’accueil, y compris sans permission de lecture générale des réglages. Vérifier les libellés FR/EN et la persistance.\n- Rétablir l’arrêt du balayage horizontal sur une page et masquer les applications de développement sur l’accueil, sans les supprimer des sources.\n- Utiliser le logo Vulpes fourni dans le splash et adapter les composants originaux de la liste des sons au moteur actuel.\n- Vérifier sur PC deux changements consécutifs de fond, le choix d’un son et les panneaux Gaia restaurés.\n\n### Android · APK\n\n- APK local Preview16 construit et signé ; contenu commun et version vérifiés. Aucune publication dans cette étape.\n- Valider Vulpes comme lanceur HOME, afficher le verrouillage Gaia après veille et rétablir le retour Android depuis la caméra ouverte au verrouillage.\n- Masquer les barres système Android en mode immersif, en conservant leur accès temporaire par balayage.\n- Tests sur émulateur Android 16/API36 : verrouillage, caméra/retour, FR/EN, accueil sans applications de test, fond d’écran et son. Fond conservé après arrêt forcé et relance, sans changement répété pendant 15 secondes. Autres appareils Android non qualifiés dans cette étape.\n\n### Tundra · Sargo\n\n- Inclure les mêmes correctifs et la politique des applications dans une nouvelle image de boot de test. Le boot précédent réappliquait une copie ancienne des fichiers et annulait les modifications à chaud.\n- Démarrage en RAM sur le Pixel réussi : six contrôles de langues/accueil/sélecteurs et dix contrôles système passent. Sélecteurs image/son vérifiés sans écraser les choix personnels. Le boot installé reste Preview15 ; aucune partition flashée dans cette étape.\n\n**Limites :** l’installation tierce FFAA reste ouverte. Le site dépend d’anciennes API mozApps et d’un script nomodule ; un installateur isolant les applications et leurs permissions reste à construire. Le choix d’un son Gaia ne modifie pas la sonnerie globale Android ; l’audio d’appel réel n’a pas été qualifié ici.", - "en": "## 2.7-preview.16 — APK built · 5 October 2026\n\n### Shared · Gaia / Desktop\n\n- Restore Gaia wallpaper and sound pickers, return navigation to Settings and cancellation through Home. Share persisted image and sound transport between desktop and Android.\n- Propagate language changes to Home without granting general settings read access. Check French/English labels and persistence.\n- Restore horizontal page snapping and hide development apps from Home without deleting their sources.\n- Use the supplied Vulpes logo in the splash and adapt the original sound-list components to the current engine.\n- Verify two consecutive wallpaper changes, sound selection and restored Gaia panels on desktop.\n\n### Android · APK\n\n- Build and sign local Preview16; verify shared payload and version. Nothing published in this step.\n- Validate Vulpes as the HOME launcher, restore Gaia locking after sleep and Android Back navigation from the lockscreen camera.\n- Hide Android system bars in immersive mode while retaining temporary swipe access.\n- Android 16/API36 emulator checks: locking, camera/Back, French/English, Home without test apps, wallpaper and sound. Wallpaper retained after force-stop/relaunch with no repeated changes over 15 seconds. Other Android devices not qualified in this step.\n\n### Tundra · Sargo\n\n- Include shared fixes and app policy in a new test boot image. The previous boot reapplied an old file snapshot, undoing hot updates.\n- RAM boot on Pixel passed: six language/Home/picker checks and ten system checks. Image/sound picker results verified without replacing personal selections. Installed boot remains Preview15; no partition flashed in this step.\n\n**Limitations:** third-party FFAA installation remains open. The site depends on old mozApps APIs and a nomodule script; an installer with application and permission isolation still needs implementation. Selecting a Gaia sound does not change the global Android ringtone; actual call audio was not qualified here." + "fr": "## 2.7-preview.16 — APK construit · 5 octobre 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sélecteurs Gaia de fond d’écran et de sons rétablis, avec retour vers Paramètres et annulation par Accueil.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Changement de langue appliqué à l’accueil et conservé après redémarrage.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Arrêt du balayage horizontal sur chaque page ; applications de développement masquées sur l’accueil.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Logo Vulpes au démarrage et liste des sons adaptée au moteur actuel. Transport des médias partagé entre PC et Android.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android : verrouillage Gaia après veille et retour depuis la caméra du verrouillage. Mode lanceur HOME validé.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Android : barres système masquées en mode immersif, accessibles par balayage. APK Preview16 construit et signé.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC et émulateur Android 16/API 36 : fonds d’écran, sons et panneaux vérifiés ; langue, verrouillage et persistance du fond testés sur Android.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : image de démarrage mise à jour pour ne plus restaurer d’anciens fichiers.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo en RAM : 6 contrôles d’interface et 10 contrôles système réussis. Installation conservée en Preview15, sans flash.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) APK non publié. Installation FFAA et audio d’appel à valider ; le choix d’un son Gaia ne change pas la sonnerie Android.\n", + "en": "## 2.7-preview.16 — APK built · 5 October 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Restored Gaia wallpaper and sound pickers, return to Settings and cancellation through Home.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Apply language changes to Home and preserve them after restart.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Restore horizontal page snapping; hide development apps on Home.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Vulpes startup logo and sound list adapted to the current engine. Media transport shared by desktop and Android.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android: Gaia locking after sleep and Back from the lockscreen camera. HOME launcher mode validated.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Android: hide system bars in immersive mode, with swipe access. Preview16 APK built and signed.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop and Android 16/API 36 emulator: checked wallpapers, sounds and panels; tested language, locking and wallpaper persistence on Android.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: updated the boot image to stop restoring outdated files.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo in RAM: 6 UI and 10 system checks passed. Installed build remains Preview15, without flashing.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) APK not published. FFAA installation and call audio await validation; Gaia sound selection does not change the Android ringtone.\n" + }, + "metadata": { + "kind": "version", + "status": "built", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "2026-10-04-sargo-standalone", "text": { - "fr": "## En développement — 4 octobre 2026 — Installation autonome Sargo\n\n### Tundra · Sargo\n\n- Construction d’un volume ext4 neuf directement destiné à userdata : système, données et composants matériels vendor/modem embarqués. Le démarrage ne cherche plus de fichiers dans une installation Droidian ; aucune session Android ni connexion ADB n’est nécessaire à l’installation.\n- Installateur Fastboot en Python : contrôle du modèle Sargo, du déverrouillage, des tailles et des empreintes, puis remplacement de userdata, dtbo_a, vbmeta_a et boot_a. Le bootloader reste déverrouillé ; les calibrations propres au téléphone sont conservées.\n- Sauvegarde complète de userdata hors ligne, comparaison intégrale avec le téléphone et vérification de l’archive de restauration avant le flash. Les sauvegardes restent privées ; une restauration réelle n’a pas été effectuée dans cette étape.\n- Deux démarrages depuis le stockage interne réussis : onze contrôles Gaia et dix contrôles natifs par démarrage, image de boot relue et vérifiée, réglages conservés. Le slot A est marqué réussi uniquement après validation du démarrage Gaia courant.\n- Variante générique sans serveur SSH ni clés de maintenance : flash de boot_a et démarrage réel réussis. La variante de développement est ensuite remise sur le Pixel pour poursuivre les travaux. Les images du paquet générique conservent leur état initial vierge.\n- Contrôles finaux sur le démarrage autonome de développement : Photo (dont bascules de capteur), clavier Gaia et notifications réussis.\n- Paquet Fastboot local assemblé avec empreintes et rapport de qualification. 44 tests ciblés passent. Il n’est pas encore publié : la provenance et la redistribution de certains composants matériels restent à finaliser.\n- Limites : qualification sur un Pixel 3a, pas sur toutes les combinaisons de firmware ; volume de 8 Gio comprenant 1 Gio de données modifiables, sans extension automatique. Les fonctionnalités matérielles restent expérimentales.\n\n### PC / Android · APK\n\n- Aucun changement d’interface ni nouveau binaire dans cette étape. Gaia reste partagé entre les trois plateformes.", - "en": "## In development — 4 October 2026 — Standalone Sargo installation\n\n### Tundra · Sargo\n\n- Build a fresh ext4 volume directly for userdata, including the system, writable data and vendor/modem hardware images. Startup no longer looks for files in a Droidian installation; installation requires neither an Android session nor ADB.\n- Python Fastboot installer checks the Sargo model, unlock state, partition sizes and checksums, then replaces userdata, dtbo_a, vbmeta_a and boot_a. The bootloader stays unlocked and device-specific calibration is retained.\n- Full offline userdata backup, complete comparison against the phone and recovery archive verification before flashing. Backups remain private; an actual restoration was not performed in this step.\n- Two successful boots from internal storage: eleven Gaia and ten native checks per boot, verified boot-image readback and retained settings. Slot A is marked successful only after validating Gaia startup in the current boot.\n- Generic variant without an SSH server or maintenance keys: actual boot_a flash and boot passed. The development variant was then restored on the Pixel for further work. Generic package images retain their original fresh state.\n- Final checks on the standalone development boot passed for Camera (including lens switches), the Gaia keyboard and notifications.\n- Local Fastboot package assembled with checksums and a qualification report. 44 targeted tests pass. It is not published yet: provenance and redistribution of some hardware components still need final review.\n- Limits: qualified on one Pixel 3a, not every firmware combination; an 8 GiB volume including 1 GiB of writable data, without automatic expansion. Hardware functionality remains experimental.\n\n### Desktop / Android · APK\n\n- No interface changes or new binary in this step. Gaia remains shared by all three platforms." + "fr": "## En développement — 4 octobre 2026 — Installation autonome Sargo\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : volume ext4 autonome pour userdata, avec système, données et composants vendor/modem. Installation indépendante de Droidian et d’ADB.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Installateur Fastboot avec contrôles du modèle, du déverrouillage, des tailles et des empreintes. Remplacement de userdata, dtbo_a, vbmeta_a et boot_a ; calibrations conservées.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sauvegarde complète de userdata et archive de restauration vérifiées avant le flash. Restauration réelle non testée.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Deux démarrages installés réussis : 11 contrôles Gaia et 10 contrôles natifs par démarrage, réglages conservés et boot relu conforme.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Variante sans SSH ni clés de maintenance démarrée après flash. Variante de développement réinstallée ; caméra, clavier et notifications vérifiés.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Paquet Fastboot local assemblé ; 44 tests ciblés réussis.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Paquet non publié : provenance et redistribution de composants matériels à finaliser. Essais sur un seul Pixel 3a ; volume de 8 Gio dont 1 Gio modifiable, sans extension automatique.\n", + "en": "## In development — 4 October 2026 — Standalone Sargo installation\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: standalone ext4 userdata volume with system, data and vendor/modem components. Installation independent of Droidian and ADB.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Fastboot installer with model, unlock, size and checksum checks. Replaces userdata, dtbo_a, vbmeta_a and boot_a; retains calibration.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Full userdata backup and recovery archive verified before flashing. Actual restoration not tested.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Two installed boots passed: 11 Gaia and 10 native checks per boot, retained settings and matching boot readback.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Variant without SSH or maintenance keys booted after flashing. Development variant reinstalled; camera, keyboard and notifications checked.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Local Fastboot package assembled; 44 targeted tests passed.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Package unpublished: hardware component provenance and redistribution pending. Tested on one Pixel 3a; 8 GiB volume with 1 GiB writable, without automatic expansion.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "2026-10-03-sargo-per-device-installation", "text": { - "fr": "## En développement — 3 octobre 2026 — Installation Sargo par appareil\n\n### Tundra · Sargo\n\n- Collecte des six partitions de démarrage en lecture seule et du stockage existant, liés à l’identité du téléphone et au slot A. Les sauvegardes restent privées.\n- Dossier appareil portable : construction RAM, contrôle du boot, installation et export du secours peuvent l’utiliser sans l’inventaire partagé du Pixel de développement. Un build lié à un dossier appareil refuse le repli implicite vers cet inventaire.\n- Transfert SSH contrôlé : vérification de l’identité et du stockage, contrôle des empreintes et création des fichiers sans remplacer les données existantes. Aucun formatage, flash ou redémarrage dans l’outil de transfert.\n- Construction et transfert testés sur le Pixel. Premier boot RAM : onze contrôles Gaia et dix contrôles natifs réussis ; clavier et notifications réussis. Le contrôle caméra a rencontré un délai d’autofocus dépassé, conservé dans le rapport.\n- Second boot de la même image : réglage Gaia conservé, onze contrôles Gaia et dix contrôles natifs réussis ; caméra, clavier et notifications réussis. Le timeout autofocus du premier essai reste documenté, pas déclaré corrigé.\n- Retour au build installé vérifié : démarrage Gaia réussi et partition de boot inchangée. Un nouveau transfert refuse bien de remplacer les données déjà utilisées.\n- Provenance du HAL précisée : paquet et recette de téléchargement identifiés, recettes UBports historiques récupérées. La correspondance exacte avec l’ancien artefact Android et son manifeste de sources reste à établir.\n- Prise en charge toujours limitée au noyau qualifié et au stockage ext4/LVM compatible. Ce travail ne fournit pas encore une installation directe depuis Android d’origine.\n\n### PC / Android · APK\n\n- Aucun changement d’interface ni nouvelle compilation dans cette étape.", - "en": "## In development — 3 October 2026 — Per-device Sargo installation\n\n### Tundra · Sargo\n\n- Read-only collection of six boot partitions and existing storage, bound to the phone identity and slot A. Backups remain private.\n- Portable device workspace: RAM builds, boot checks, installation and recovery export can use it without the development Pixel’s shared inventory. Workspace-bound builds reject implicit fallback to that inventory.\n- Checked SSH transfers verify identity, storage and checksums and create files without replacing existing data. The transfer tool never formats, flashes or reboots.\n- Build and transfer tested on the Pixel. First RAM boot passed eleven Gaia and ten native checks; keyboard and notifications passed. Camera testing encountered an autofocus timeout, retained in the report.\n- Second boot of the same image retained the Gaia setting and passed eleven Gaia and ten native checks; camera, keyboard and notifications passed. The first autofocus timeout remains documented, not declared fixed.\n- Verify return to the installed build: Gaia starts and the boot partition is unchanged. A repeated transfer correctly refuses to replace existing data.\n- Refine HAL provenance: identify the installed package and download recipe and retrieve historical UBports build recipes. Exact correspondence with the old Android artifact and resolved source manifest remains unverified.\n- Still limited to the qualified kernel and compatible ext4/LVM storage. This does not yet provide direct installation from stock Android.\n\n### Desktop / Android · APK\n\n- No interface changes or new build in this step." + "fr": "## En développement — 3 octobre 2026 — Installation Sargo par appareil\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : dossier appareil portable pour les builds, l’installation et le secours, indépendant de l’inventaire du Pixel de développement.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Collecte en lecture seule des six partitions de démarrage et du stockage, liée à l’identité du téléphone. Sauvegardes privées.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Transfert SSH avec contrôle d’identité, du stockage et des empreintes, sans écrasement, formatage ni flash.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Deux boots RAM : 11 contrôles Gaia et 10 contrôles natifs réussis chacun ; réglage conservé. Timeout autofocus au premier essai, caméra fonctionnelle au second.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Retour au build installé vérifié ; boot inchangé et écrasement des données refusé.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Timeout autofocus non déclaré corrigé. Provenance exacte du HAL encore à établir ; noyau et stockage ext4/LVM compatibles requis, sans installation directe depuis Android d’origine.\n", + "en": "## In development — 3 October 2026 — Per-device Sargo installation\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: portable device workspace for builds, installation and recovery, independent of the development Pixel inventory.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Read-only collection of six boot partitions and storage, bound to the phone identity. Private backups.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) SSH transfer with identity, storage and checksum checks, without overwriting, formatting or flashing.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Two RAM boots: 11 Gaia and 10 native checks passed each; setting retained. Autofocus timed out on the first trial; camera worked on the second.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Return to the installed build verified; boot unchanged and data overwrite rejected.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Autofocus timeout not considered fixed. Exact HAL provenance still pending; qualified kernel and compatible ext4/LVM storage required, without direct installation from stock Android.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "2026-10-03-sargo-installation-tools", "text": { - "fr": "## En développement — 3 octobre 2026 — Préparation et secours Sargo\n\n### Tundra · Sargo\n\n- Ajout d’un outil de préparation locale : contrôle du manifeste et de l’archive, décompression bornée, vérification ext4 et création d’un espace de données vierge propre à l’installation. Les dossiers existants ne sont pas écrasés.\n- Ajout d’un kit de restauration privé exportable hors du dépôt. Il conserve la sauvegarde originale de boot_a et un outil autonome qui vérifie le modèle, le numéro de série, le slot et les empreintes avant toute restauration explicite. Il ne constitue pas une sauvegarde des données utilisateur.\n- La restauration existante ne dépend plus de la présence de l’image de diagnostic. Les contrôles de la sauvegarde sont conservés.\n- Construction système : référence matérielle ARM64 désormais explicite, avec refus d’une référence limitée à l’affichage ou dépourvue des services requis.\n- Préparation de l’archive système réelle et vérification locale du kit de secours effectuées. Aucun nouveau flash ni nouvelle ROM publiée ; l’installation automatique sur un autre Sargo reste à finaliser.\n\n### Commun · PC / Android / Tundra\n\n- Le journal JSON partagé est conservé dans les futures publications de sources ; les documents de travail Markdown restent exclus.\n\n### PC / Android · APK\n\n- Aucune modification fonctionnelle ni nouvelle compilation dans cette étape.", - "en": "## In development — 3 October 2026 — Sargo preparation and recovery tools\n\n### Tundra · Sargo\n\n- Add local package preparation: verify manifest and archive checksums, bound decompression, check ext4 and create a fresh data image for each installation. Existing directories are never overwritten.\n- Add a private recovery kit that can be exported outside the checkout. It retains the original boot_a backup and a standalone tool that checks model, serial number, slot and checksums before explicit restoration. It is not a user-data backup.\n- Existing restoration no longer requires the diagnostic image to remain available. Backup validation is retained.\n- System builds now require an explicit ARM64 hardware reference and reject display-only references or missing required services.\n- Prepare the real system archive and verify the recovery kit locally. No new flash or public ROM release; automated installation on another Sargo still needs completion.\n\n### Shared · Desktop / Android / Tundra\n\n- Retain the shared JSON changelog in future source publications; internal Markdown working documents remain excluded.\n\n### Desktop / Android · APK\n\n- No functional changes or new build in this step." + "fr": "## En développement — 3 octobre 2026 — Préparation et secours Sargo\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : préparation locale des paquets avec contrôles d’empreintes, décompression bornée, vérification ext4 et création de données vierges sans écrasement.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Kit de secours privé avec sauvegarde de boot_a et contrôles du modèle, du numéro de série, du slot et des empreintes. Données utilisateur non incluses.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Restauration possible sans l’image de diagnostic.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Construction système : référence matérielle ARM64 complète exigée.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Archive système préparée et kit de secours vérifié localement. Installation sur un autre Sargo à finaliser ; aucun flash ni ROM publiée.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Journal JSON inclus dans les prochaines publications de sources ; documents de travail exclus.\n", + "en": "## In development — 3 October 2026 — Sargo preparation and recovery tools\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: local package preparation with checksum checks, bounded extraction, ext4 validation and fresh data creation without overwriting.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Private recovery kit with boot_a backup and model, serial, slot and checksum checks. User data not included.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Restoration works without the diagnostic image.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) System builds require a complete ARM64 hardware reference.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) System archive prepared and recovery kit checked locally. Installation on another Sargo pending; no flash or ROM release.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) JSON changelog included in future source releases; working documents excluded.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "2026-10-03-sargo-modem-persistence", "text": { - "fr": "## En développement — 3 octobre 2026 — Sargo : récupération modem et installation vierge\n\n### Tundra · Sargo\n\n- Reconstruction de l’image système avec le correctif du cache des polices et la création du répertoire privé d’oFono au premier démarrage.\n- Récupération limitée au sous-système modem : test contrôlé avec passage hors ligne, remise en service et réenregistrement réseau en environ 22 secondes, sans redémarrage du téléphone. Cela ne corrige pas la cause du watchdog IMS du firmware.\n- Démarrage Gaia, rendu matériel, polices Fira, deux capteurs photo, focus/zoom, clavier et notifications visuelles contrôlés sur le profil neuf. Scan Wi-Fi encore fonctionnel après le redémarrage du modem.\n- Deux démarrages réussis avec conservation du même réglage Gaia sur un espace de données séparé. Caméra, clavier et notifications revérifiés au second démarrage. La partition de boot installée et les données existantes sont conservées.\n- Aucune nouvelle image native publiée : le paquet public d’installation/restauration reste à terminer.\n\n### PC / Android · APK\n\n- Aucun changement de l’interface commune ni nouvelle compilation APK dans cette étape.", - "en": "## In development — 3 October 2026 — Sargo modem recovery and fresh installation\n\n### Tundra · Sargo\n\n- Rebuild the system image with the font-cache fix and creation of oFono’s private state directory on first boot.\n- Limit recovery to the modem subsystem: a controlled restart went offline, recovered and registered on the network in about 22 seconds without rebooting the phone. This does not fix the firmware’s IMS watchdog root cause.\n- Check Gaia startup, hardware rendering, Fira fonts, both camera sensors, focus/zoom, keyboard and visual notifications with a fresh profile. Wi-Fi scanning still works after the modem restart.\n- Two successful boots retained the same Gaia setting in a separate data image. Camera, keyboard and notifications rechecked after the second boot. Installed boot partition and existing data retained.\n- No new native image published: the public installation/recovery package remains unfinished.\n\n### Desktop / Android · APK\n\n- No shared interface changes or new APK build in this step." + "fr": "## En développement — 3 octobre 2026 — Sargo : récupération modem et installation vierge\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : cache des polices et création du dossier privé oFono au premier démarrage corrigés dans l’image système.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Récupération du modem sans redémarrer le téléphone. Réenregistrement réseau mesuré à environ 22 secondes lors d’un essai contrôlé.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Profil neuf : Gaia, rendu matériel, Fira, deux caméras, focus, zoom, clavier et notifications vérifiés. Scan Wi-Fi fonctionnel après redémarrage du modem.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Deux démarrages avec conservation des réglages dans un espace de données séparé. Installation existante préservée.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Cause du watchdog IMS non corrigée. Paquet public d’installation et de restauration encore incomplet ; aucune image publiée.\n", + "en": "## In development — 3 October 2026 — Sargo modem recovery and fresh installation\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: corrected font cache and first-boot creation of oFono’s private directory in the system image.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Modem recovery without rebooting the phone. Network registration took about 22 seconds in a controlled trial.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Fresh profile: checked Gaia, hardware rendering, Fira, both cameras, focus, zoom, keyboard and notifications. Wi-Fi scan worked after modem restart.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Two boots retained settings in a separate data image. Existing installation preserved.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) IMS watchdog root cause unresolved. Public installation and recovery package still incomplete; no image released.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "2026-10-01-sargo-distribution-preparation", "text": { - "fr": "## 1er octobre 2026 — Sargo : préparation de la distribution — en développement\n\n### Tundra · Sargo\n\n- Image système Preview 15 reconstruite avec profils vierges, exclusion des documents de travail et conservation des licences ; inventaire des 493 paquets embarqués.\n- Vérification ext4 et contrôle du contenu effectués. Ces contrôles ne constituent pas une validation de démarrage.\n- Inventaire du stockage en lecture seule et génération de la table de montage depuis cet inventaire, au lieu de reprendre systématiquement les dimensions du Pixel de développement. Prise en charge limitée au volume ext4 à une seule étendue sur userdata Sargo ; aucun formatage ajouté.\n- Image nettoyée transférée et vérifiée sur le Pixel chargé. Essais temporaires en RAM, sans écriture de boot_a ; retour automatique vers le build v59 et redémarrage de Gaia contrôlés.\n- SIM reconnue, enregistrement LTE et scan Wi-Fi contrôlés sur cette image ; ces contrôles ne valident pas les appels, SMS ou données mobiles de bout en bout.\n- Correction des droits du cache des polices au premier démarrage : fc-cache utilise le compte de session. Démarrage Gaia, rendu matériel, Fira, deux capteurs photo, clavier et notifications visuelles contrôlés en RAM. La qualification du fichier reconstruit reste incomplète.\n- Redémarrage intermittent identifié dans le firmware IMS du modem ; politique de récupération ciblée appliquée et second démarrage avec LTE, caméra, clavier et notifications réussi. La récupération après un nouveau crash IMS reste non testée. L’image publique reste non flashable et non publiée.\n\n### PC / APK\n\n- Aucun changement fonctionnel ni nouvelle compilation APK dans cette étape.", - "en": "## October 1, 2026 — Sargo distribution preparation — in development\n\n### Tundra · Sargo\n\n- Rebuild Preview 15 system image with empty profiles, internal working documents excluded, license notices retained and an inventory of 493 runtime packages.\n- Filesystem and content checks completed; these checks do not qualify hardware boot.\n- Read-only storage inventory and mount-table generation from that inventory instead of hardcoded development-phone dimensions. Limited to a single ext4 extent on Sargo userdata; no formatting added.\n- Clean image transferred and verified on the charged Pixel. Temporary RAM boot tests left boot_a unchanged; automatic return to installed v59 and Gaia startup checked.\n- SIM detection, LTE registration and Wi-Fi scanning checked on this image; these checks do not establish end-to-end calling, SMS or mobile data.\n- Fix first-boot font cache ownership by running fc-cache as the session user. Gaia startup, hardware rendering, Fira, both camera sensors, keyboard and visual notifications checked in RAM. Qualification of the rebuilt image remains incomplete.\n- Intermittent reboot traced to modem IMS firmware; targeted recovery policy applied and another boot with LTE, camera, keyboard and notifications passed. Recovery after a new IMS crash has not been exercised. The public image remains non-flashable and unpublished.\n\n### Desktop / APK\n\n- No functional changes or new APK build in this step." + "fr": "## 1er octobre 2026 — Sargo : préparation de la distribution — en développement\n\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo : image Preview15 reconstruite avec profils vierges, licences et inventaire des 493 paquets. Documents de travail exclus.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Table de montage générée depuis l’inventaire du stockage. Prise en charge limitée à une seule étendue ext4 sur userdata.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Droits du cache des polices au premier démarrage : fc-cache exécuté avec le compte de session.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Contrôles ext4, contenu et transfert réussis. Essais RAM sans écriture de boot_a ; retour au build v59 vérifié.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) SIM, LTE, scan Wi-Fi, Gaia, rendu matériel, Fira, caméras, clavier et notifications vérifiés en RAM.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Récupération ciblée après identification d’un redémarrage intermittent du firmware IMS. Second démarrage réussi ; nouveau crash IMS non testé.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Qualification de l’image incomplète, sans validation des appels, SMS ou données mobiles de bout en bout. Image non publiée et non flashable.\n", + "en": "## October 1, 2026 — Sargo distribution preparation — in development\n\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo: rebuilt Preview15 image with empty profiles, licenses and an inventory of 493 packages. Working documents excluded.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Mount table generated from the storage inventory. Support limited to a single ext4 extent on userdata.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) First-boot font cache permissions: run fc-cache as the session user.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Filesystem, content and transfer checks passed. RAM trials left boot_a unchanged; return to v59 verified.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) SIM, LTE, Wi-Fi scan, Gaia, hardware rendering, Fira, cameras, keyboard and notifications checked in RAM.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Targeted recovery after tracing intermittent reboots to IMS firmware. Second boot passed; recovery from another IMS crash not tested.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Image qualification incomplete; end-to-end calls, SMS and mobile data unvalidated. Image unpublished and non-flashable.\n" + }, + "metadata": { + "kind": "history", + "status": "history", + "platforms": [ + "pc", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "2.7-preview.15", "text": { - "fr": "## 2.7-preview.15 — 30 septembre 2026 — publiée\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15)\n\n### Commun · Gaia / PC / Android / Tundra\n\n- Première publication des sources communes, avec README français et anglais, licences et scripts de préparation. Documentation interne, roadmap, profils, clés et images privées exclus du dépôt public.\n- Intégration des derniers correctifs SMS (dates des bulles, états d’envoi/échec) et du cycle de vie caméra. Les deux entrées du 30 septembre détaillent les correctifs matériels.\n\n### PC\n\n- Copie de publication testée avec moteur Gecko 156.0.1 et profil neuf : démarrage Gaia, navigation, réglages, gestes, historique et isolation des pages. 42 tests de services passants ; persistance des services après redémarrage vérifiée.\n- Stabilisation du test de navigation lors du remplacement normal d’un document et allongement de l’attente de démarrage à froid. Fermeture propre de l’hôte même si Gaia dépasse le délai de préparation.\n\n### Android · APK\n\n- Preview 15 signée avec la clé des précédentes Preview, GeckoView 156.0 ; parité des ressources communes vérifiée. Mise à jour installée sur émulateur Android 16.\n- Photo et Galerie : capture, affichage et reprise du capteur validés. Dix passages en arrière-plan et récupération après arrêt du processus d’affichage validés. Cette qualification ne couvre pas tous les téléphones Android.\n\n### Tundra · Sargo\n\n- Sources du runtime incluses ; l’image privée v59 reste celle qualifiée sur le téléphone de développement. Aucune ROM générique flashable publiée : démarrage encore lié au stockage inventorié, aux images préinstallées et aux accès de maintenance privés.\n- Limites conservées : aperçu caméra proche de 16 i/s, flash/vidéo, rotation et veille profonde incomplets ; audio d’appel à qualifier et grand dialogue après envoi SMS à identifier.\n", - "en": "## 2.7-preview.15 — 30 September 2026 — published\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15)\n\n### Shared · Gaia / Desktop / Android / Tundra\n\n- First shared source publication, including English/French READMEs, licenses and setup scripts. Internal documents, roadmap, profiles, keys and private images excluded from the public repository.\n- Include the latest SMS fixes (bubble dates, sent/failed states) and camera lifecycle fixes. The two 30 September entries detail the hardware changes.\n\n### Desktop PC\n\n- Published checkout tested with Gecko 156.0.1 and a fresh profile: Gaia startup, navigation, settings, gestures, history and page isolation. 42 service tests pass; service persistence after restart checked.\n- Stabilize navigation checks during normal document replacement and allow more time for cold startup. Close the host cleanly even if Gaia exceeds its preparation timeout.\n\n### Android · APK\n\n- Preview 15 signed with the previous Preview key, GeckoView 156.0; shared resource parity checked. Installed as an update on an Android 16 emulator.\n- Photo/Gallery capture, image display and sensor resume validated. Ten background/resume cycles and recovery after renderer termination validated. This does not qualify every Android phone.\n\n### Tundra · Sargo\n\n- Runtime sources included; private v59 remains the build qualified on the development phone. No generic flashable ROM published: boot still depends on inventoried storage, preinstalled images and private maintenance access.\n- Limits remain: approximately 16 fps camera preview, incomplete flash/video, rotation and deep suspend; call audio awaiting qualification and a large dialog after SMS sending still under investigation.\n" + "fr": "## 2.7-preview.15 — 30 septembre 2026 — publiée\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Première publication des sources PC, Android et Tundra, avec README FR/EN, licences et scripts de préparation. Fichiers privés exclus.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS : dates des bulles et états d’envoi/échec corrigés. Libération et remplacement des flux caméra fiabilisés.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) PC : tests de navigation stabilisés, délai de démarrage à froid ajusté et fermeture propre après dépassement du délai Gaia.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC avec Gecko 156.0.1 : 42 tests de services, démarrage, navigation, réglages, gestes, historique, isolation et persistance vérifiés.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) APK Preview15 signé avec la clé précédente et GeckoView 156.0 ; parité des ressources vérifiée.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Émulateur Android 16 : mise à jour, Photo/Galerie, reprise caméra, dix retours d’arrière-plan et récupération du rendu validés.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo : sources incluses, build privé v59 conservé. Aucune ROM générique ; démarrage lié au stockage et aux images préinstallées.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo : aperçu proche de 16 i/s ; flash, vidéo, rotation et veille profonde incomplets. Audio d’appel et dialogue après envoi SMS à examiner.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15)\n", + "en": "## 2.7-preview.15 — 30 September 2026 — published\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) First publication of desktop, Android and Tundra sources, with French/English READMEs, licenses and setup scripts. Private files excluded.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS: corrected bubble dates and sent/failed states. Improved camera stream cleanup and replacement.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Desktop: stabilized navigation checks, adjusted cold-start timeout and clean shutdown after Gaia times out.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop with Gecko 156.0.1: 42 service tests, startup, navigation, settings, gestures, history, isolation and persistence checked.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Preview15 APK signed with the previous key and GeckoView 156.0; asset parity verified.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Android 16 emulator: validated update, Camera/Gallery, camera resume, ten background returns and renderer recovery.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo: sources included, private v59 retained. No generic ROM; boot depends on storage layout and preinstalled images.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo: preview around 16 fps; flash, video, rotation and deep suspend incomplete. Call audio and the dialog after SMS sending need investigation.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15)\n" + }, + "metadata": { + "kind": "version", + "status": "published", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "development-2026-09-30-sms-camera-followup", "text": { - "fr": "## En développement — retours SMS et caméra · 30 septembre 2026\n\n### Commun · Gaia / PC / Tundra\n\n- SMS : convertir les dates en nombres avant de construire les bulles. Corrige les conversations vides dues à une erreur de formatage de date. Parcours du compositeur et affichage des messages envoyés/reçus vérifiés sur PC avec un modem simulé.\n- Photo : fermeture du flux rendue idempotente; détacher les vidéos, attendre la libération du capteur et libérer le contexte WebGL avant de changer de caméra. Identifier chaque session pour qu’un ancien flux ne puisse pas arrêter son remplaçant. Allers-retours arrière/avant vérifiés avec de nouvelles trames à chaque bascule; retour Accueil et arrêt du capteur validés lors de deux démarrages RAM.\n\n### Tundra / Sargo\n\n- Vibreur : utiliser le contrôle brightness du pilote DRV2624; la commande legacy activate ne coupait pas physiquement le moteur. Coupure confirmée par l'utilisateur; impulsion, motif 200/200/200 ms et arrêt du service vérifiés, avec retour à zéro. Ajouter une coupure de secours au démarrage et à la sortie du service.\n- L'utilisateur confirme focus, zoom et transport SMS dans les deux sens sur v54. Cinq conversations existantes affichées sur le Pixel, avec leurs bulles et sans erreur de date.\n- v59 installé sur boot_a après deux démarrages RAM réussis; démarrage installé et relecture vérifiés, slot A marqué réussi. Sources et runtime PC synchronisés. Le grand dialogue après un envoi réel reste à identifier; la fluidité caméra reste un chantier distinct.\n\n### Android · APK\n\n- Sources Gaia communes corrigées; aucun nouvel APK construit ou publié.\n", - "en": "## In development — SMS and camera feedback · 30 September 2026\n\n### Shared · Gaia / Desktop / Tundra\n\n- Messages: serialize bubble dates as numbers. Fix empty conversations caused by date formatting errors. Composer flow and sent/received bubbles verified on desktop with a simulated modem.\n- Camera: make release idempotent; detach videos, await sensor release and dispose of the WebGL context before switching cameras. Identify each session so an old stream cannot stop its replacement. Repeated rear/front switches produce fresh frames; Home return and sensor shutdown passed on two RAM boots.\n\n### Tundra / Sargo\n\n- Vibration: use the DRV2624 brightness control; the legacy activate command did not physically stop the motor. The user confirmed stopping; single-pulse, 200/200/200 ms pattern and service-stop checks all returned to zero. Add a stop command when the service starts or exits.\n- The user confirms focus, zoom and SMS transport both ways on v54. Five existing conversations render on the Pixel, with message bubbles and no date errors.\n- v59 installed in boot_a after two successful RAM boots; installed startup and readback verified, slot A marked successful. Desktop sources and runtime synchronized. The large dialog after a real send still needs identification; camera smoothness remains separate work.\n\n### Android · APK\n\n- Shared Gaia sources updated; no APK rebuilt or published.\n" + "fr": "## En développement — retours SMS et caméra · 30 septembre 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS : conversion des dates pour corriger les conversations vides.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Caméra : fermeture des flux et libération du capteur corrigées ; une ancienne session ne peut plus fermer la suivante.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : arrêt physique du vibreur via DRV2624, avec coupure de secours au démarrage et à l’arrêt du service.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : composition et affichage des SMS vérifiés avec modem simulé. Sargo : bascules caméra et arrêt du capteur testés sur deux boots RAM.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v54 : focus, zoom et SMS dans les deux sens confirmés par l’utilisateur. Cinq conversations affichées ; arrêt du vibreur confirmé.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo v59 installé sur boot_a après deux boots RAM ; démarrage, relecture et slot A vérifiés.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Dialogue après envoi SMS et fluidité caméra encore à traiter. Sources communes mises à jour, sans nouvel APK.\n", + "en": "## In development — SMS and camera feedback · 30 September 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS: convert dates to fix empty conversations.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Camera: corrected stream and sensor cleanup; an old session can no longer close its replacement.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: physical vibration shutdown through DRV2624, with a safeguard at service startup and exit.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: SMS composition and display checked with a simulated modem. Sargo: camera switching and shutdown tested over two RAM boots.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v54: user confirmed focus, zoom and two-way SMS. Five conversations displayed; vibration shutdown confirmed.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo v59 installed on boot_a after two RAM boots; startup, readback and slot A verified.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Post-SMS dialog and camera smoothness still pending. Shared sources updated, without a new APK.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "development-2026-09-30-sms-camera", "text": { - "fr": "## En développement — SMS et Photo · 30 septembre 2026\n\n### PC / Tundra\n\n- SMS : conserver le message et sa conversation après un refus du modem; transmettre les événements Gaia « envoi », « échec » et « envoyé ».\n- SMS : distinguer mise en file oFono et confirmation d'envoi, avec suivi persistant des états terminaux. La confirmation modem n'est pas un accusé de réception du destinataire.\n- PC : tests de l'échec d'envoi visible dans la liste des conversations; Photo et Galerie testées avec capture tactile, ouverture, fermeture et reprise de la caméra.\n\n### Tundra / Sargo\n\n- Photo : raccorder les zones de focus tactile, le zoom numérique et la capture JPEG du capteur à l'interface Gaia originale. Aperçu conservé en 640 × 480; JPEG arrière 4032 × 3024 (12,19 Mpx) et avant 3264 × 2448 (7,99 Mpx) mesurés directement. Focus fixe à l'avant.\n- Protocole caméra : séparer les traces du HAL des données binaires, borner les réponses et relancer l'aperçu après une capture. Une erreur de focus ne doit plus fermer la session.\n- SMS : refuser explicitement l'envoi si le modem n'est pas enregistré; conserver les états sent/failed même après disparition de l'objet oFono. La réception a produit des notifications opérateur sur v49 après retour du réseau LTE.\n- Qualification : deux démarrages v54 en RAM puis démarrage depuis boot_a validés; image installée sur le Pixel et slot marqué réussi après relecture. Persistance, interface Gaia et tests caméra passants. L'enregistrement réseau reste instable : essais SMS arrêtés avant émission, aucune livraison de bout en bout confirmée.\n\n### Android · APK\n\n- Aucun nouvel APK construit ou publié dans cette passe. La Preview 14 disponible reste inchangée.\n", - "en": "## In development — Messages and Camera · 30 September 2026\n\n### Desktop / Tundra\n\n- Messages now retain the message and conversation after a modem rejection and dispatch Gaia sending, failed and sent events.\n- Distinguish the oFono queue from confirmed transmission; persist terminal states. Modem confirmation is not a recipient delivery receipt.\n- Desktop checks cover failed messages visible in conversations, touch capture, Gallery opening and camera release/resume.\n\n### Tundra / Sargo\n\n- Connect touch focus areas, digital zoom and sensor JPEG capture to the original Gaia Camera. Preview stays at 640 × 480; directly measured JPEG sizes are 4032 × 3024 (12.19 MP) rear and 3264 × 2448 (7.99 MP) front. The front camera has fixed focus.\n- Keep HAL logs out of the binary protocol, bound replies and resume preview after capture. A focus failure must not close the session.\n- Reject SMS submission explicitly when unregistered; retain sent/failed states after the oFono object disappears. Carrier messages triggered Gaia notifications on v49 after LTE registration recovered.\n- Qualification: two v54 RAM boots and the installed boot_a startup passed; image installed on the Pixel and slot marked successful after readback. Persistence, Gaia startup and Camera checks passed. Network registration remains unstable: SMS tests stopped before transmission, with no end-to-end delivery confirmed.\n\n### Android · APK\n\n- No APK rebuilt or published in this pass. The available Preview 14 is unchanged.\n" + "fr": "## En développement — SMS et Photo · 30 septembre 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) PC/Tundra : conservation des SMS après refus du modem et transmission des états envoi, échec et envoyé à Gaia.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : états SMS persistants après disparition de l’objet oFono ; envoi refusé si le modem n’est pas enregistré.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : focus tactile, zoom numérique et capture JPEG du capteur dans Photo Gaia.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Photos mesurées : 4032 × 3024 à l’arrière, 3264 × 2448 à l’avant. Aperçu 640 × 480 ; focus avant fixe.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Caméra native : données binaires séparées des traces HAL, réponses bornées et reprise de l’aperçu après capture ou erreur de focus.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : échec d’envoi visible et parcours Photo/Galerie vérifiés. Sargo v54 : deux boots RAM puis installation, persistance et caméra validées.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Réseau instable ; aucune livraison SMS de bout en bout confirmée dans cette passe. Confirmation modem distincte de l’accusé de réception. APK public inchangé (Preview14).\n", + "en": "## In development — Messages and Camera · 30 September 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Desktop/Tundra: retain SMS after modem rejection and pass sending, failed and sent states to Gaia.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: persist SMS states after the oFono object disappears; reject sending when the modem is unregistered.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: touch focus, digital zoom and sensor JPEG capture in Gaia Camera.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Measured photos: 4032 × 3024 rear, 3264 × 2448 front. Preview 640 × 480; fixed front focus.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Native camera: separate binary data from HAL logs, bound responses and resume preview after capture or focus errors.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: checked visible send failures and Camera/Gallery workflows. Sargo v54: validated two RAM boots, installation, persistence and camera.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Unstable network; no end-to-end SMS delivery confirmed in this pass. Modem confirmation is not a delivery receipt. Public APK unchanged (Preview14).\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "2.7-preview.14", "text": { - "fr": "## 2.7-preview.14 — APK publié · 30 septembre 2026\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.14)\n\nL’APK Preview 14 est publié. La version PC est à jour et le build privé Sargo v48 est installé et testé ; aucune image téléphone n’est publiée avec cette release.\n\n### Commun · PC / APK / Gaia\n\n- Informations : suppression du retrait supplémentaire des lignes de version avant « Dernière mise à jour » ; alignement contrôlé sur PC et Android.\n- Polices : normalisation des noms de fontes dans les feuilles des composants Shadow DOM, y compris les styles ajoutés après leur création. Les polices d’icônes restent distinctes.\n- Calendrier : ouverture du mois et absence d’erreur JavaScript vérifiées sur PC.\n\n### PC / Tundra\n\n- Clavier : calculer le mode portrait depuis l’écran, et non depuis la petite fenêtre du clavier. Touches de 60 px CSS environ sur Pixel ; rotation et retour au portrait testés sur PC à densité 3.\n- SMS : replacer le curseur dans le champ actif lorsque Gecko conserve la sélection du destinataire ; conserver les sélections valides pour l’édition.\n- Notifications Gaia : respecter les indicateurs silencieux, sans son et sans vibration, y compris à la restauration.\n\n- Clavier Gaia : pont de saisie natif, pavé numérique pour les destinataires SMS, événements de sélection corrigés et protection contre les réponses d’un champ devenu inactif. Saisie destinataire et message testée sur PC et Pixel ; l’envoi opérateur reste à qualifier.\n- Verrouillage : accepter l’absence de fenêtre clavier lors d’un redimensionnement.\n- Notifications natives : stockage, propriété par application et ouverture du point d’entrée Téléphone déclaré par Gaia. Présentation et suppression dans le volet et sur le verrouillage validées sur Sargo ; écoute du son à qualifier.\n\n### Tundra / Sargo — build v48 installé\n\n- Vibrations : pont vers le moteur Sargo, motifs bornés à dix secondes, séquencement sans bloquer les boutons physiques et annulation. Activation et arrêt mesurés sur le pilote.\n\n- Wi-Fi : profils privés conservés avec reconnexion automatique et commande d’oubli ; tests du stockage et de la restauration en cas d’échec. Connexion et reconnexion après redémarrage RAM vérifiées sur Sargo ; fichiers de profil privés (0600).\n- SMS entrants : stockage persistant avant acquittement, dédoublonnage des reprises et services créant leur dossier d’état au démarrage. Défaut de montage du dossier corrigé après reproduction sur Sargo.\n- Audio : commandes écouteur/haut-parleur, micro et volume d’appel ajoutées au pont ; bascule écouteur/haut-parleur, sourdine et volume vérifiés sans appel. Conversation dans les deux sens encore à vérifier.\n- Images de diagnostic avec débogueur local interdites au flash par l’outil d’installation. v48/preview.14 écrite sur boot_a après deux démarrages RAM et contrôle de persistance ; démarrage installé validé, image relue conforme et slot A marqué réussi. v43 à v48 n’activent pas de débogueur.\n\n### Android · APK\n\n- APK preview.14 construit, signé et publié sur Forgejo. Démarrage, informations de version, alignement et sélection des fontes des composants contrôlés sur Android 16 émulé. Le clavier Android reste celui du système.", - "en": "## 2.7-preview.14 — APK published · 30 September 2026\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.14)\n\nThe Preview 14 APK is published. Desktop is up to date and the private Sargo v48 build is installed and tested; this release does not include a public phone image.\n\n### Shared · PC / APK / Gaia\n\n- About: remove the extra indentation in version rows above “Last updated”; alignment checked on desktop and Android.\n- Fonts: normalize text font names inside Shadow DOM stylesheets, including styles added later; keep icon fonts intact.\n- Calendar: month view opens without JavaScript errors in the desktop check.\n\n### PC / Tundra\n\n- Keyboard: use the screen orientation instead of the short keyboard frame. Keys measure about 60 CSS pixels on Pixel; rotation tested on desktop at DPR 3.\n- SMS: restore the caret to the focused field when Gecko retains the recipient selection; preserve valid selections during editing.\n- Gaia notifications: respect silent, no-sound and no-vibration flags, including restored notifications.\n\n- Gaia keyboard: native input bridge, numeric SMS recipient layout, corrected selection events and protection against stale field replies. Recipient and message typing tested on desktop and Pixel; carrier delivery still requires qualification.\n- Lockscreen: tolerate the absence of a keyboard window during resize.\n- Native notifications: persistent application-owned entries and launch through Gaia's declared Phone entry point. Tray and lockscreen display/removal verified on Sargo; audible sound still needs qualification.\n\n### Tundra / Sargo — v48 build installed\n\n- Vibration: Sargo actuator bridge, patterns limited to ten seconds, non-blocking sequencing and cancellation. Driver activation and stop measured.\n\n- Wi-Fi: private persistent profiles, autoconnect and forget operation; storage and failed-update rollback tests. Connection and reconnection after a RAM reboot verified on Sargo; private profile files (0600).\n- Incoming SMS: persist before acknowledgement and deduplicate retries. Services now create their state directory at startup, fixing a mount failure reproduced on Sargo.\n- Call audio: earpiece/speaker, microphone mute and call volume controls added; earpiece/speaker switching, mute and volume verified without placing a call. Two-way conversation still unverified.\n- Local-debugger images cannot be flashed by the installer. v48/preview.14 written to boot_a after two RAM boots and persistence checks; installed boot verified, image readback matches and slot A marked successful. v43–v48 do not enable a debugger.\n\n### Android · APK\n\n- Preview.14 APK built, signed and published on Forgejo. Startup, version information, alignment and component font selection checked on an Android 16 emulator. Android retains its system keyboard." + "fr": "## 2.7-preview.14 — APK publié · 30 septembre 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Informations système : alignement des versions et polices Fira dans les composants Shadow DOM.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) PC/Tundra : taille des touches en portrait, curseur dans le champ actif et réponses de saisie obsolètes.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) PC/Tundra : pont clavier Gaia et pavé numérique pour les destinataires SMS. Clavier Android inchangé.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Verrouillage : redimensionnement sans fenêtre clavier. Notifications silencieuses respectées, y compris après restauration.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : notifications persistantes dans le volet et sur le verrouillage, avec ouverture de l’application concernée.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : vibrations annulables, limitées à dix secondes ; profils Wi-Fi privés persistants, reconnexion automatique et oubli du réseau.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : stockage des SMS entrants avant acquittement, dédoublonnage et création du dossier d’état.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : commandes écouteur/haut-parleur, micro muet et volume d’appel. Bascule, sourdine et volume testés hors appel.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Installation : flash des images de diagnostic avec débogueur refusé. Sargo v48 installé après deux boots RAM ; persistance, relecture et slot A vérifiés.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : Calendrier et clavier vérifiés. Sargo : clavier, notifications et reconnexion Wi-Fi vérifiés ; son des notifications et téléphonie réelle à valider.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) APK Preview14 signé et publié. Démarrage, versions et polices vérifiés sur émulateur Android 16. Aucune image Sargo publiée.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.14)\n", + "en": "## 2.7-preview.14 — APK published · 30 September 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) System information: version alignment and Fira fonts in Shadow DOM components.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Desktop/Tundra: portrait key sizing, cursor placement in the active field and stale input replies.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Desktop/Tundra: Gaia keyboard bridge and numeric keypad for SMS recipients. Android keeps its system keyboard.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Lockscreen: resize without a keyboard window. Honour silent notifications, including after restoration.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: persistent tray and lockscreen notifications, opening the owning application.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: cancellable vibrations capped at ten seconds; private persistent Wi-Fi profiles, automatic reconnection and network removal.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: store incoming SMS before acknowledgement, remove duplicates and create the state directory.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: earpiece/speaker, microphone mute and call-volume controls. Routing, mute and volume tested outside a call.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Installer: reject flashing diagnostic images with a debugger. Sargo v48 installed after two RAM boots; persistence, readback and slot A verified.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: Calendar and keyboard checked. Sargo: keyboard, notifications and Wi-Fi reconnection checked; notification sound and real telephony await validation.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Preview14 APK signed and published. Startup, versions and fonts checked on an Android 16 emulator. No Sargo image released.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.14)\n" + }, + "metadata": { + "kind": "version", + "status": "published", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "2.7-preview.13", "text": { - "fr": "## 2.7-preview.13 — parcours dans les applications · 27 septembre 2026\n\n### Commun · Gaia / PC\n\n- Calendrier : une alarme planifiée avec succès ne provoque plus un rejet lors de la sauvegarde de l’événement; les erreurs de planification sont transmises au demandeur.\n- Rappels : ignorer ceux d’événements supprimés; ne plus marquer une notification refusée comme livrée. La prise en charge des notifications natives Tundra reste à terminer.\n- Notifications : accepter un texte brut vide sans chercher une traduction « undefined ».\n- Galerie : vignettes de taille positive même au démarrage masqué; annulation des mises à jour d’aperçu après fermeture de l’éditeur.\n- SMS : appliquer les filtres correspondant, lu/non lu, période et livraison; tests de lecture et suppression des conversations.\n- Revue approfondie : lecture, déplacement et suppression d’une vidéo WebM; édition noir et blanc et copie d’une photo; création et suppression d’un événement. Interfaces Gaia originales conservées.\n\n- Qualification depuis une copie isolée : démarrage, tests bureau, persistance et empaquetage Android réussis sans réécriture des sources.\n\n### Android · APK\n\n- APK preview.13 construit et signé; Photo/Galerie, Calendrier, dix retours d’arrière-plan et récupération du rendu validés sur Android 16 émulé.\n\n### Tundra · Sargo\n\n- Build v40/preview.13 installé après deux démarrages RAM : Gaia, caméra, Wi-Fi, luminosité et commandes contrôlés; relecture de boot_a conforme et slot validé. Limites restantes dans `docs/APPLICATION-REVIEW.md`.", - "en": "## 2.7-preview.13 — application workflows · 27 September 2026\n\n### Shared · Gaia / Desktop\n\n- Calendar: successfully scheduling a reminder no longer rejects event saving; scheduling failures reach the caller.\n- Reminders: ignore deleted events and do not mark denied notifications as delivered. Native Tundra notification support remains incomplete.\n- Notifications: accept empty raw text without requesting an “undefined” translation.\n- Gallery: positive thumbnail size even when starting hidden; cancel preview updates after leaving the editor.\n- SMS: apply correspondent, read state, date range and delivery filters; cover conversation reading and deletion.\n- Deeper review: WebM playback, seeking and deletion; black-and-white photo editing and saved copy; event creation and deletion. Retain original Gaia interfaces.\n\n- Isolated checkout qualification passes startup, desktop tests, persistence and Android packaging without rewriting sources.\n\n### Android · APK\n\n- Signed preview.13 APK built; Camera/Gallery, Calendar, ten background/resume cycles and renderer recovery validated on an Android 16 emulator.\n\n### Tundra · Sargo\n\n- Build v40/preview.13 installed after two RAM boots: Gaia, camera, Wi-Fi, brightness and controls checked; matching boot_a readback and successful slot marking. Remaining limits in `docs/APPLICATION-REVIEW.md`." + "fr": "## 2.7-preview.13 — parcours dans les applications · 27 septembre 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Calendrier : sauvegarde des événements avec alarme, remontée des erreurs et exclusion des rappels d’événements supprimés.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Notifications : texte vide accepté ; notification refusée non marquée comme livrée. Support natif Tundra encore incomplet.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Galerie : taille des vignettes au démarrage masqué et arrêt des mises à jour après fermeture de l’éditeur.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS : filtres par correspondant, lecture, période et livraison appliqués.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : vidéo WebM, édition et copie de photo, calendrier, lecture et suppression des conversations vérifiés.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Copie isolée : démarrage, tests PC, persistance et empaquetage Android réussis.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) APK Preview13 construit et signé. Photo/Galerie, Calendrier, dix reprises d’arrière-plan et récupération du rendu validés sur Android 16 émulé.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v40 installé après deux boots RAM ; Gaia, caméra, Wi-Fi, luminosité, commandes et relecture de boot_a vérifiés.\n", + "en": "## 2.7-preview.13 — application workflows · 27 September 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Calendar: save events with alarms, report scheduling errors and skip reminders for deleted events.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Notifications: accept empty text; do not mark rejected notifications as delivered. Native Tundra support still incomplete.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Gallery: thumbnail sizing at hidden startup and cancellation of updates after closing the editor.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS: apply correspondent, read-state, date and delivery filters.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: checked WebM playback, photo editing and copying, calendar, conversation reading and deletion.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Isolated checkout: startup, desktop tests, persistence and Android packaging passed.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Preview13 APK built and signed. Camera/Gallery, Calendar, ten background returns and renderer recovery validated on emulated Android 16.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v40 installed after two RAM boots; Gaia, camera, Wi-Fi, brightness, controls and boot_a readback verified.\n" + }, + "metadata": { + "kind": "version", + "status": "built", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "2.7-preview.12", "text": { - "fr": "## 2.7-preview.12 — revue des applications · 27 septembre 2026\n\n### Commun · Gaia / PC\n\n- Icônes internes : variantes raster Gaia originales de 1× à 2,25× rétablies dans\n 1 047 références CSS. Les icônes vectorielles et les polices restent originales.\n- Photo : bouton de changement de capteur réactivé après découverte des caméras;\n flash indisponible visible et grisé; le pincement ne zoome plus le document Gaia.\n- Accueil : gestes natifs réservés au bouton pendant l’appui long; menu contextuel\n désactivé sur ce bouton. Le gestionnaire des tâches Gaia reste utilisé.\n- Transmission des gestes entre System et les applications restaurée avec des\n événements DOM non privilégiés, sans autoriser de faux gestes sur les sites Web.\n- Navigateur : intégration de la page `browser-welcome` fournie, locale, bilingue,\n sans suivi; recherches et liens transmis au véritable navigateur.\n- Page d’accueil : suit automatiquement la langue Gaia, `fr`/`fr-*` en français,\n les autres en anglais; changement appliqué sans redémarrage.\n- SMS : identifiants compatibles avec le registre numérique Gaia, migration des\n anciens messages, dates de l’API et événement d’envoi corrigés; écritures sérialisées.\n- Calendrier : libellé d’accessibilité du jour courant retrouvé dans 37 langues.\n- Revue de 14 applications; 26 contrôles fonctionnels Galerie/Musique/Horloge/UI,\n tests Contacts et SMS, et navigation Web isolée. Voir `docs/APPLICATION-REVIEW.md`.\n\n### Android · APK\n\n- APK `2.7-preview.12` : même interface, correctifs et page locale; reconstruction\n et parité contrôlées. L’accès caméra reste celui d’Android via getUserMedia.\n- Aucun portage automatique des fonctions matérielles Tundra vers Android : chaque\n plateforme conserve son adaptateur et annonce ses capacités réelles.\n\n### Tundra · Sargo\n\n- Aperçu Photo : conversion NV21 par shader, transport binaire au lieu de base64;\n environ 23 images/s mesurées en RAM contre environ 8 dans l’essai intermédiaire.\n- Fermeture du worker attendue avant de rouvrir un autre capteur.\n- Luminosité manuelle : retrait des animations successives, environ 200 ms pour\n atteindre la valeur demandée lors du test matériel; transitions veille/réveil conservées.\n- Wi-Fi : état radio réel et activation/désactivation raccordés aux réglages Gaia;\n réseau mixte WPA2/WPA3 compatible avec le formulaire WPA-PSK.\n- Scan par Gaia, connexion et navigation HTTPS par wlan0 validés en RAM. Les\n profils de connexion restent temporaires; leur persistance est encore à réaliser.\n- Image v38 installée : deux démarrages RAM, démarrage installé, relecture de\n `boot_a`, préservation des autres partitions et validation du slot A réussis.\n- Flash photo, vidéo, capteur de luminosité/rotation, Bluetooth, comptage de données\n et fonctions avancées d’appel restent incomplets. Un écran qui s’ouvre ne vaut\n pas validation de tout son matériel.", - "en": "## 2.7-preview.12 — application review · 27 September 2026\n\n### Shared · Gaia / Desktop\n\n- Restore original Gaia 1×–2.25× raster variants for 1,047 CSS image references.\n- Camera switch becomes available after device discovery; unavailable flash stays\n visible and disabled; pinching no longer zooms the whole camera application.\n- Reserve Home gestures for long press and suppress its context menu; retain Gaia's task manager.\n- Relay Gaia touch gestures using unprivileged DOM events, without granting trusted\n user activation to external websites.\n- Bundle the supplied bilingual, local browser welcome page and route links and\n searches into the actual browser.\n- Welcome page follows the Gaia language live: `fr`/`fr-*` select French;\n all other languages select English.\n- SMS: numeric identifiers, migration of older records, Date values, sent events,\n multi-recipient request shape and serialized storage writes.\n- Calendar: restore the current day's accessibility label in 37 locales.\n- Review 14 apps; 26 media/clock/UI checks, contact and SMS checks, plus real Web\n navigation and isolation. Scope and limitations: `docs/APPLICATION-REVIEW.md`.\n\n### Android · APK\n\n- Rebuild `2.7-preview.12` with the same shared UI and local welcome page; check\n payload parity. Android camera capture continues to use getUserMedia.\n\n- Tundra hardware features are not automatically ported to Android: each platform\n retains its adapter and reports its actual capabilities.\n\n### Tundra · Sargo\n\n- Convert NV21 using a shader and transfer binary frames instead of base64:\n approximately 23 fps measured in RAM, versus 8 fps in the intermediate trial.\n- Wait for camera worker exit before opening the other sensor.\n- Manual brightness reaches its target in about 200 ms in the hardware test;\n keep screen sleep/wake fades.\n- Connect Gaia Wi-Fi power settings to actual radio state; support WPA-PSK on\n mixed WPA2/WPA3 networks.\n- Scan, connection and HTTPS over wlan0 validated in RAM; connection profiles are temporary.\n- Wi-Fi profiles are still temporary. Flash, video, light/orientation sensors,\n Bluetooth, data counters and advanced call controls remain incomplete.\n- v38 installed: two RAM boots, installed startup, `boot_a` readback, other boot\n partitions preserved and slot A marked successful." + "fr": "## 2.7-preview.12 — revue des applications · 27 septembre 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Icônes Gaia : variantes de 1× à 2,25× restaurées dans 1 047 références CSS.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Photo : changement de capteur après détection et désactivation du zoom de l’interface au pincement. Flash indisponible grisé.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Accueil : appui long et transmission des gestes rétablis, sans privilèges supplémentaires pour les sites web.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Navigateur : page d’accueil locale bilingue, suivant la langue Gaia sans redémarrage.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS : identifiants, migration, dates, événements d’envoi, requêtes multi-destinataires et écritures concurrentes corrigés.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Calendrier : libellé d’accessibilité du jour courant rétabli dans 37 langues.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Revue de 14 applications ; 26 contrôles Galerie/Musique/Horloge/UI, tests Contacts/SMS et isolation web.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) APK Preview12 reconstruit avec les mêmes ressources ; parité vérifiée. Fonctions matérielles adaptées séparément sur Android et Tundra.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo : aperçu caméra NV21 par shader et transport binaire, environ 23 i/s mesurées contre 8 dans l’essai intermédiaire.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : fermeture du worker caméra attendue avant changement de capteur ; luminosité manuelle réglée en environ 200 ms.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : activation Wi-Fi depuis Gaia et connexion WPA-PSK aux réseaux mixtes WPA2/WPA3.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : scan, connexion et HTTPS validés en RAM. v38 installé après deux boots RAM, relecture et slot A vérifiés.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo : profils Wi-Fi temporaires ; flash, vidéo, capteurs, Bluetooth, comptage de données et appels avancés incomplets.\n", + "en": "## 2.7-preview.12 — application review · 27 September 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Gaia icons: restored 1× to 2.25× variants across 1,047 CSS references.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Camera: enable switching after sensor detection and prevent pinch zoom on the UI. Unavailable flash disabled.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Home: restore long press and gesture forwarding without additional privileges for websites.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Browser: local bilingual welcome page following Gaia’s language without restarting.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) SMS: corrected identifiers, migration, dates, sending events, multi-recipient requests and concurrent writes.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Calendar: restored the current-day accessibility label in 37 languages.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Reviewed 14 apps; 26 Gallery/Music/Clock/UI checks, Contacts/SMS tests and web isolation checks.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Preview12 APK rebuilt with shared assets; parity verified. Hardware features handled separately on Android and Tundra.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Sargo: shader-based NV21 preview and binary transport, about 23 fps measured versus 8 in the intermediate trial.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: wait for camera worker shutdown before switching sensors; manual brightness reaches its target in about 200 ms.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: Wi-Fi power control from Gaia and WPA-PSK connections to mixed WPA2/WPA3 networks.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: scan, connection and HTTPS validated in RAM. v38 installed after two RAM boots, with readback and slot A checks.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo: temporary Wi-Fi profiles; flash, video, sensors, Bluetooth, data counters and advanced call controls incomplete.\n" + }, + "metadata": { + "kind": "version", + "status": "built", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "development-2026-09-27-4", "text": { - "fr": "## En développement — retours Sargo et interface · 27 septembre 2026\n\n### Commun · Gaia / PC\n\n- Animation d’arrêt : trois nuances bleues remplacent les ronds orange; dimensions,\n séquence et durées de l’animation Gaia sont conservées.\n\n- Téléphone : empêcher le défilement du document lors du changement d’onglet;\n les en-têtes Journal et Contacts restent en place. Les icônes du journal utilisent\n les images Gaia originales en plusieurs densités, jusqu’à 2,25×.\n- Menus déroulants : retrait du décalage prévu pour les anciens moteurs, qui coupait\n les jambages de Fira. Vidéo : texte de présentation contenu dans la fenêtre.\n- Luminosité : le curseur enregistre un nombre et répond aux événements input/change;\n les anciennes valeurs textuelles sont converties avant la transition Gaia. La file\n matérielle regroupe les pas obsolètes de luminosité sans réordonner veille/réveil.\n- Accueil : le fond d’écran continue sous le dégradé transparent. L’espace de navigation\n reste réservé dans les applications; en paysage, le bouton se place à droite.\n- Navigateur : page locale éditable dans `overrides/search.gaiamobile.org/home/index.html`.\n Les branchements de Gaia et le mode privé restent en place.\n- Wi-Fi : un échec affiche une explication et le bouton de reprise au lieu de relancer\n indéfiniment le scanner. La recherche et l’affichage dans le panneau Gaia ont\n maintenant été vérifiés sur Sargo en RAM.\n- Suivi consolidé : `docs/CHANTIER.md`, avec anciens et nouveaux retours, preuves et limites.\n\n### Android · APK\n\n- APK `2.7-preview.11` construit et signé avec la même base Gaia; parité vérifiée.\n Barre Accueil à droite en paysage et correction du fond en mode Accueil.\n Rotation et absence de recouvrement vérifiées sur émulateur Android 16.\n\n### Tundra · Sargo\n\n- Caméra : aperçu matériel dans Photo Gaia, JPEG et bascule arrière/avant validés\n en RAM sur Sargo; les capteurs se ferment au retour à l’accueil. Capture initiale\n limitée à 640 × 480 (480 × 640 en portrait); vidéo et flash non disponibles.\n L’interface reste commune au PC et à l’APK, qui conservent getUserMedia.\n\n- v33 (`2.7-preview.11`) installé après deux démarrages RAM réussis. Démarrage installé, relecture de\n `boot_a`, conservation des données et finalisation du slot A vérifiés.\n- Luminosité mesurée depuis Gaia : 35 % → 89/255, 80 % → 204/255. Résultats Wi-Fi\n visibles dans le panneau original. Animation d’arrêt bleue incluse.\n- Routage audio : prise en charge du format de PulseAudio 14; suppression du double\n chargement des modules audio. Passage au profil téléphonique et à l’écouteur, puis\n restauration du profil média, vérifiés sur Sargo. Conversation réelle encore à valider.\n- Restent ouverts : conversation audible dans les deux sens, capture pleine résolution/vidéo et\n raccordement des capteurs pour la rotation automatique. Voir la liste de suivi.", - "en": "## In development — Sargo feedback and interface · 27 September 2026\n\n### Shared · Gaia / Desktop\n\n- Shutdown animation: three blue shades replace the orange rings; original Gaia\n dimensions, sequence and timing are preserved.\n\n- Phone: prevent document scrolling on tab changes, keeping Recents and Contacts\n headers in place. Use Gaia’s original call-log artwork at multiple densities up to 2.25×.\n- Select controls: remove the old engine offset that clipped Fira descenders.\n Video: keep the introductory text within the viewport.\n- Brightness: save a number from input/change events; normalize legacy text values\n before Gaia animates the transition. Coalesce stale hardware brightness steps\n while preserving the order of sleep/wake commands.\n- Home: extend wallpaper beneath the transparent gradient, reserve navigation space\n inside applications, and place Home on the right in landscape.\n- Browser: editable local page at `overrides/search.gaiamobile.org/home/index.html`,\n retaining Gaia integration and private mode.\n- Wi-Fi: show a scan failure and retry action instead of endless retries. Network\n discovery and results in the original Gaia panel now verified on Sargo in RAM.\n- Consolidated tracking: `docs/CHANTIER.md` includes earlier and new feedback and limitations.\n\n### Android · APK\n\n- Build and sign `2.7-preview.11` with shared Gaia; payload parity checked.\n Place Home on the right in landscape and fix its wallpaper background.\n Check rotation and non-overlapping app/navigation bounds on Android 16 emulator.\n\n### Tundra · Sargo\n\n- Camera: hardware preview in Gaia, JPEG capture and rear/front switching verified\n on Sargo in RAM; sensors close on returning Home. Capture is currently limited\n to 640 × 480 (480 × 640 portrait); video and flash remain unavailable.\n Desktop and APK keep getUserMedia and share the same Gaia interface.\n\n- v33 (`2.7-preview.11`) installed after two successful RAM boots. Installed boot, `boot_a` readback,\n data preservation and slot A success finalization verified.\n- Backlight measured from Gaia: 35% → 89/255, 80% → 204/255. Wi-Fi results visible\n in the original panel. Blue shutdown rings included.\n- Call audio: support PulseAudio 14 text output and prevent loading audio modules twice.\n Voice-call profile, earpiece selection and media-profile restoration verified on Sargo.\n An actual two-way conversation remains unvalidated.\n- Still open: audible two-way calls, full-resolution camera capture/video and sensor-driven rotation.\n See the tracking document." + "fr": "## En développement — retours Sargo et interface · 27 septembre 2026\n\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Animation d’arrêt Gaia recolorée en bleu.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Téléphone : en-têtes fixes au changement d’onglet et icônes du journal adaptées aux densités jusqu’à 2,25×.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Texte coupé dans les menus déroulants et débordement du texte d’accueil Vidéo.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Luminosité : valeurs numériques et regroupement des commandes obsolètes, avec ordre veille/réveil préservé.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Accueil : fond sous le dégradé et bouton à droite en paysage, sans recouvrir les applications.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Navigateur : page d’accueil locale modifiable, avec mode privé conservé.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Wi-Fi : message d’erreur et bouton Réessayer à la place des relances sans fin.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) APK Preview11 construit et signé ; rotation et position du bouton Accueil vérifiées sur Android 16 émulé.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : aperçu, JPEG et bascules caméra validés en RAM. Capture limitée à 640 × 480 ; flash et vidéo indisponibles.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v33 installé après deux boots RAM ; démarrage, données, relecture et slot A vérifiés. Luminosité et résultats Wi-Fi contrôlés dans Gaia.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : compatibilité PulseAudio 14 et suppression du double chargement des modules audio. Profils appel/média et écouteur testés.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo : conversation audio, capture pleine résolution, vidéo et rotation automatique à valider.\n", + "en": "## In development — Sargo feedback and interface · 27 September 2026\n\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Gaia shutdown animation recoloured blue.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Phone: keep headers fixed when changing tabs and use call-log icons up to 2.25× density.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Clipped dropdown text and overflowing Video welcome text.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Brightness: numeric values and coalesced stale commands, preserving sleep/wake order.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Home: wallpaper under the gradient and right-side button in landscape, without covering apps.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Browser: editable local welcome page, retaining private mode.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Wi-Fi: error message and Retry button instead of endless retries.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Preview11 APK built and signed; rotation and Home button placement checked on emulated Android 16.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: preview, JPEG and camera switching validated in RAM. Capture limited to 640 × 480; flash and video unavailable.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v33 installed after two RAM boots; startup, data, readback and slot A verified. Brightness and Wi-Fi results checked in Gaia.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: PulseAudio 14 compatibility and duplicate audio module loading. Call/media profiles and earpiece tested.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Sargo: call audio, full-resolution capture, video and automatic rotation await validation.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "development-2026-09-27-3", "text": { - "fr": "## En développement — stabilité Wi-Fi native · 27 septembre 2026\n\n### Commun · Gaia / PC\n\n- Correction de la boucle Wi-Fi sans délai : recherche toutes les 20 secondes,\n conformément à l’intervalle prévu par Gaia. Regroupement des scans simultanés.\n- Version de développement commune : `2.7-preview.9`, Gaia `2.7.0`; versions\n réelles de Gecko inchangées. Tests de rafales, erreurs et temporisation ajoutés.\n\n### Android · APK\n\n- Nouveau paquet `2.7-preview.9` pour conserver la même base Gaia. Le Wi-Fi Android\n reste géré par Android; ce correctif ne prétend pas ajouter un pilote à l’APK.\n\n### Tundra · Sargo\n\n- Cause du blocage v21/v22 identifiée : multiplication des clients Python de\n recherche Wi-Fi, épuisement mémoire et arrêt de Gecko par le lowmemorykiller.\n Le SIGSEGV de phoc survient ensuite pendant sa fermeture.\n- Un seul scan matériel en cours, cache court des résultats et des erreurs,\n invalidation après connexion/déconnexion. Aucun affaiblissement de l’isolation\n des applications, ni désactivation du mécanisme de protection mémoire.\n- Candidat v25 qualifié sur deux démarrages RAM, puis installé et vérifié sur le\n Pixel : 11 contrôles Gaia et 10 contrôles système passent, sans destruction de\n processus par manque de mémoire. Verrouillage Gaia, Fira, batterie, date et\n panneau Wi-Fi contrôlés; veille/réveil et volume testés par événements injectés.\n- Relecture de `boot_a` conforme, autres partitions de démarrage inchangées,\n données conservées et slot A marqué comme démarré avec succès. La caméra native\n et la validation complète des appels restent à terminer.", - "en": "## In development — native Wi-Fi stability · 27 September 2026\n\n### Shared · Gaia / Desktop\n\n- Fix the Wi-Fi loop with no delay: scan every 20 seconds as intended by Gaia.\n Coalesce concurrent scans. Add burst, failure and timer regression tests.\n- Shared development version: `2.7-preview.9`, Gaia `2.7.0`; actual Gecko versions\n remain unchanged.\n\n### Android · APK\n\n- Build `2.7-preview.9` to retain shared Gaia parity. Android still manages its\n own Wi-Fi; this change does not claim to add a hardware driver to the APK.\n\n### Tundra · Sargo\n\n- Identify the v21/v22 failure: unbounded Python Wi-Fi scan clients exhausted\n memory, causing the lowmemorykiller to terminate Gecko. The phoc SIGSEGV occurs\n afterwards during display teardown.\n- Allow one hardware scan at a time, briefly cache results and errors, invalidate\n after connect/disconnect. Application isolation and memory protection remain enabled.\n- Qualify v25 through two RAM boots, then install and verify it on the Pixel:\n 11 Gaia and 10 system checks pass, with no memory-pressure process kills.\n Check the Gaia lockscreen, Fira, battery, date and Wi-Fi panel; exercise\n sleep/wake and volume through injected input events.\n- Verify `boot_a` by readback, preserve the other boot partitions and user data,\n and mark slot A successful. Native camera support and full call validation\n remain unfinished." + "fr": "## En développement — stabilité Wi-Fi native · 27 septembre 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Wi-Fi : intervalle de recherche rétabli à 20 secondes et regroupement des scans simultanés.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : un seul scan matériel à la fois, avec cache court et invalidation à la connexion/déconnexion. Corrige l’épuisement mémoire des builds v21/v22.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Tests de rafales, erreurs et temporisation Wi-Fi.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Version commune Preview9, Gaia 2.7.0 ; nouvel APK avec les mêmes sources. Wi-Fi Android toujours géré par Android.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v25 : deux boots RAM puis installation, 11 contrôles Gaia et 10 système réussis, sans arrêt de processus par manque de mémoire.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Verrouillage, polices, batterie, date et Wi-Fi vérifiés ; veille/réveil et volume testés par événements injectés. Données, relecture et slot A contrôlés.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Caméra native et validation complète des appels encore à terminer.\n", + "en": "## In development — native Wi-Fi stability · 27 September 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Wi-Fi: restore the 20-second scan interval and coalesce concurrent scans.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: one hardware scan at a time, with a short cache cleared on connect/disconnect. Fixes memory exhaustion in v21/v22.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Wi-Fi burst, failure and timer tests.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Shared Preview9 version, Gaia 2.7.0; new APK with the same sources. Android still manages its own Wi-Fi.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v25: two RAM boots then installation, 11 Gaia and 10 system checks passed, without low-memory process kills.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Checked locking, fonts, battery, date and Wi-Fi; tested sleep/wake and volume with injected events. Data, readback and slot A verified.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Native camera and full call validation still pending.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "development-2026-09-27-2", "text": { - "fr": "## En développement — restauration Gaia · 27 septembre 2026\n\n### Commun · Gaia / PC\n\n- Restauration des panneaux Gaia et suppression des fenêtres/boutons provisoires :\n Paramètres, Suivi conso, FM et import de contacts/médias. Les fonctions sans API\n restent visibles avec commandes désactivées, sans valeurs matérielles inventées.\n- Correction de la portée des anciens styles : les sélecteurs `div` ne sont plus\n transformés en racines de composants. Retour des icônes du verrouillage,\n conservation de Fira et glyphes Gaia vectoriels pour les onglets Téléphone.\n- Accueil dispose de 44 pixels CSS réservés et ne recouvre plus Décrocher/Raccrocher.\n La date utilise le composant historique; la batterie est actualisée dans Paramètres\n et ses libellés n’affichent plus `{{ level }}`.\n- Écran de verrouillage Gaia réactivé; splash provisoire avec le logo fourni,\n dimensions stables et animation douce. Nom localisé : Navigateur / Browser.\n Remerciements dans Informations légales, avis de licences conservés.\n- Tests PC : 26 contrôles applicatifs passent (Galerie, Musique, Horloge, tâches),\n panneaux, imports, batterie, date et navigation; appels simulés,\n journal, veille/réveil, menu Power et volume. Voir `docs/GAIA-RESTORATION.md`.\n\n### Android · APK\n\n- APK `2.7-preview.8` construit et installé sur l’émulateur Android 16, avec la même base Gaia; batterie lue depuis Android,\n Paramètres Android en bas de liste et mode Accueil Android optionnel. Le catalogue\n des applications Android dans Gaia reste à raccorder.\n- Validation Android : dix reprises après changement d’application et récupération\n après destruction du renderer. Parité du contenu partagé vérifiée; pas de test\n physique Android dans cette passe.\n- Les imports utilisent les composants Gaia; le bouton SMS ouvre la confirmation\n Android. Le verrouillage interne ne remplace pas la sécurité Android.\n\n### Tundra · Sargo\n\n- Candidats v21/v22 construits avec tous les correctifs `overrides/` dans le paquet de boot;\n raccordement scan/association Wi-Fi au panneau Gaia via NetworkManager.\n- Essais RAM bloqués : service graphique arrêté avec le code 139 avant qualification.\n Aucun nouveau flash. Retour sur v20 installé validé (11 contrôles Gaia, 9 système,\n données conservées); verrouillage du profil natif remis à son état précédent.\n Wi-Fi du candidat, caméra native, API SIM/Bluetooth et suspension CPU non qualifiés.", - "en": "## In development — Gaia restoration · 27 September 2026\n\n### Shared · Gaia / Desktop\n\n- Restore Gaia panels and remove temporary windows/floating controls: Settings,\n Usage, FM and contact/media import. Features without an API remain visible with\n disabled controls, without invented hardware values.\n- Fix legacy style scoping: ordinary `div` selectors no longer become component\n roots. Restore lockscreen icons, retain Fira and use scalable Gaia glyphs for\n Phone tabs.\n- Reserve 44 CSS pixels for Home so it cannot cover Answer/Hang up. The date uses\n its original component; battery updates reach Settings and its labels no longer\n display `{{ level }}`.\n- Re-enable the original Gaia lockscreen; temporary splash uses the supplied logo,\n stable dimensions and a gentle animation. Localized name: Navigateur / Browser.\n Credits appear in Legal Information; original license notices remain.\n- All 26 application checks pass (Gallery, Music, Clock, task management). Further\n desktop checks cover panels, imports, battery, date and navigation; simulated\n calls, history, sleep/wake, Power menu and volume. See `docs/GAIA-RESTORATION.md`.\n\n### Android · APK\n\n- Build and install `2.7-preview.8` on the Android 16 emulator with shared Gaia; read battery state from Android,\n add Android Settings at the bottom and optional Android Home mode. The Android\n application catalogue in Gaia still needs integration.\n- Android checks: ten background/resume cycles and renderer termination recovery;\n shared payload parity verified. No physical Android device test in this pass.\n- Imports use Gaia components; the SMS button opens Android confirmation.\n The internal lockscreen does not replace Android security.\n\n### Tundra · Sargo\n\n- Build v21/v22 candidates with all `overrides/` fixes in the boot payload; connect Wi-Fi\n scan/association to the Gaia panel through NetworkManager.\n- RAM trials blocked: the display service exits with code 139 before qualification.\n No new flash. Restore installed v20 and verify 11 Gaia and 9 system checks with\n preserved data; restore the native profile’s previous unlocked state. Candidate\n Wi-Fi, native camera, full SIM/Bluetooth APIs and CPU suspend remain unqualified." + "fr": "## En développement — restauration Gaia · 27 septembre 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Panneaux Gaia restaurés : Paramètres, Suivi conso, FM et imports. Interfaces provisoires retirées ; fonctions indisponibles désactivées.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Styles des anciens composants, icônes du verrouillage et glyphes Téléphone corrigés.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Espace de 44 pixels réservé au bouton Accueil ; date et batterie rétablies, sans texte `{{ level }}`.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Verrouillage Gaia réactivé, splash stabilisé, nom Navigateur/Browser localisé et crédits ajoutés.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : 26 contrôles applicatifs réussis, plus panneaux, imports, batterie, navigation et appels simulés.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) APK Preview8 installé sur Android 16 émulé : batterie Android, accès aux paramètres et mode HOME optionnel. Catalogue Android encore incomplet.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Android émulé : dix reprises d’arrière-plan, récupération du rendu et parité des ressources vérifiées. Verrouillage Gaia distinct de la sécurité Android.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo v21/v22 : scan et association Wi-Fi raccordés à Gaia via NetworkManager.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : essais RAM interrompus par l’arrêt du service graphique (code 139). Retour au v20 installé vérifié, sans nouveau flash.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Wi-Fi du candidat, caméra, API SIM/Bluetooth et suspension CPU non validés.\n", + "en": "## In development — Gaia restoration · 27 September 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Restored Gaia Settings, Usage, FM and import panels. Temporary interfaces removed; unavailable features disabled.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Corrected legacy component styles, lockscreen icons and Phone glyphs.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Reserve 44 pixels for Home; restore date and battery without `{{ level }}` placeholders.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Gaia lockscreen re-enabled, splash stabilized, Browser name localized and credits added.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: 26 app checks passed, plus panels, imports, battery, navigation and simulated calls.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Preview8 APK installed on emulated Android 16: Android battery, settings access and optional HOME mode. Android app catalogue still incomplete.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Emulated Android: ten background returns, renderer recovery and asset parity checked. Gaia locking remains separate from Android security.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo v21/v22: Wi-Fi scan and association connected to Gaia through NetworkManager.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: RAM trials interrupted by display-service failure (code 139). Return to installed v20 verified, without a new flash.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Candidate Wi-Fi, camera, SIM/Bluetooth APIs and CPU suspend unvalidated.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "development-2026-09-27-1", "text": { - "fr": "## En développement — 27 septembre 2026\n\n### Commun · Gaia / PC\n\n- Remplacement de l’écran d’appel provisoire par **Callscreen**, l’interface\n originale de Gaia : appel entrant/sortant, réponse, raccrochage et fermeture.\n L’adaptateur fournit les événements `mozTelephony` aux modules historiques.\n Les contrôles audio/conférence non encore raccordés restent grisés.\n- Journal d’appels persistant côté hôte, puis import dans le vrai journal Gaia,\n avec acquittement et détection des doublons après interruption.\n- Batterie : pont vers l’icône historique ; une batterie inconnue n’est pas\n présentée comme vide. Correction de l’index de contacts Facebook absent ou\n mal formé, qui bloquait aussi la recherche de numéro et l’historique local.\n- Polices : comparaison des glyphes réellement rendus. L’accueil utilisait\n déjà Fira Sans des deux côtés, mais certains textes système utilisaient Noto\n sur PC. Les règles de texte principales sélectionnent désormais la Fira de\n Gaia ; les polices d’icônes sont conservées.\n- Test Gecko avec modem simulé : appels entrants/sortants, boutons Répondre et\n Raccrocher, journal, veille/réveil, menu Power, volume et batterie.\n Aucun nouvel APK compilé dans cette étape.\n\n- Étape v13 (remplacée par Callscreen ci-dessus) : écran d’appel géré par l’application système : numérotation, sonnerie,\n communication et bouton Raccrocher. Les contrôles restent présents si le\n service perd momentanément l’état de l’appel.\n- Tests sur le vrai Gecko bureau avec modem simulé, y compris échec puis nouvel\n essai de raccrochage. Sources PC synchronisées ; aucun APK reconstruit.\n\n### Tundra · Sargo\n\n- Build privé v20 installé et relu dans `boot_a`, après deux démarrages RAM\n et un démarrage depuis le stockage ; réglages conservés, slot marqué réussi.\n- Lecture des seules touches Power/Volume du Sargo et\n transmission à `HardwareButtons`. Appui court : écran éteint/rallumé ; appui\n long : menu Gaia. Le broker natif commande rétroéclairage, volume, arrêt et\n redémarrage avec une liste d’opérations fixe.\n- Sérialisation des changements de luminosité et d’alimentation : deux\n requêtes simultanées ne peuvent plus laisser le rétroéclairage éteint au réveil.\n- Volume natif : accès en lecture au socket PulseAudio de la session, sans\n lequel l’indicateur Gaia changeait mais la sortie audio restait inchangée.\n- La veille écran ne suspend pas encore le noyau. Réception réelle des appels,\n audio de conversation et commandes audio avancées restent à qualifier.\n\n- Étape précédente : build privé v13 installé dans `boot_a` après deux démarrages RAM ; démarrage\n depuis le stockage, relecture et indicateur de réussite A/B vérifiés.\n- Transmission des appels et de leurs états depuis oFono ; commandes Répondre et\n Raccrocher limitées aux chemins d’appel du modem.\n- Correction des réponses D-Bus sans contenu : un raccrochage réussi ne provoque\n plus une fausse erreur de réponse invalide.\n- Un appel réel court atteint la sonnerie puis est raccroché automatiquement.\n Cela ne qualifie pas encore l’audio en conversation ni les appels entrants.\n- Caméra : diagnostic corrigé, sans annoncer une initialisation à partir d’un\n simple délai d’attente. Deux capteurs sont détectés via libhybris, mais le test\n de flux reçoit zéro image. La caméra Gaia native reste indisponible ; voir la\n [note technique](tundra/docs/CAMERA-SARGO.md).", - "en": "## In development — 27 September 2026\n\n### Shared · Gaia / Desktop\n\n- Replace the temporary call overlay with Gaia’s original **Callscreen**:\n incoming/outgoing calls, answer, hang up and automatic close. A compatibility\n adapter supplies `mozTelephony` events to the original modules. Unsupported\n audio/conference controls remain disabled.\n- Persist completed calls in the host and import them into Gaia’s original call\n log, with acknowledgement and duplicate detection after interruption.\n- Connect real battery state to the original status icon; unknown is not empty.\n Handle a missing/malformed Facebook contacts index without blocking local\n number lookup or call history.\n- Compare rendered font faces: home already used Fira on both platforms, but\n desktop system text partly used Noto. Main text rules now select Gaia’s\n bundled Fira face while preserving icon fonts.\n- Desktop Gecko test with a simulated modem covers incoming/outgoing UI,\n answer/hang up, history, screen sleep/wake, power menu, volume and battery.\n No APK rebuilt in this step.\n\n- Earlier v13 step (superseded above): system-owned call screen with dialing, ringing, connected state and Hang up.\n Controls remain available if the service temporarily loses call status.\n- Tested in desktop Gecko with a simulated modem, including hang-up failure and\n retry. Desktop sources synchronized; no APK rebuilt.\n\n### Tundra · Sargo\n\n- Private v20 installed and read back in `boot_a` after two RAM boots and one\n installed boot; settings retained and slot marked successful.\n- Read only Sargo’s physical Power/Volume keys and forward\n their edges to Gaia HardwareButtons. Short press sleeps/wakes the screen;\n long press opens Gaia’s menu. The restricted native broker controls backlight,\n volume, shutdown and reboot.\n- Serialized brightness and power changes: concurrent requests no longer\n leave the backlight disabled while waking the screen.\n- Native volume: expose the session PulseAudio socket read-only to the service;\n otherwise Gaia’s indicator changed while the audio output remained unchanged.\n- Screen sleep does not suspend the kernel yet. Real incoming calls, conversation\n audio and advanced call audio controls remain to be qualified.\n\n- Earlier step: private v13 installed in `boot_a` after two RAM boots; installed boot,\n readback and the A/B success flag verified.\n- oFono call state reporting and Answer/Hang up commands restricted to modem call paths.\n- Successful void D-Bus responses no longer cause a false invalid-response error.\n- A short real outgoing call reached ringing and was automatically hung up.\n Conversation audio and incoming calls remain unqualified.\n- Camera diagnostics no longer mistake a timeout for successful initialization.\n Libhybris detects two sensors, but the preview probe receives no frames.\n Native Gaia camera access remains unavailable." + "fr": "## En développement — 27 septembre 2026\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Callscreen Gaia pour les appels entrants/sortants, avec réponse et raccrochage. Remplace l’écran provisoire v13.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Journal d’appels persistant avec acquittement et dédoublonnage.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Batterie inconnue distincte d’une batterie vide ; index Contacts absent ou mal formé toléré.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Textes système affichés avec Fira Sans, sans modifier les polices d’icônes.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC avec modem simulé : appels, raccrochage en échec puis nouvel essai, journal, veille/réveil, menu Power, volume et batterie.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : boutons Power/Volume raccordés à Gaia et commandes natives d’écran, volume, arrêt et redémarrage.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : luminosité et alimentation sérialisées pour éviter l’écran noir au réveil ; accès PulseAudio corrigé pour le volume.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : un raccrochage réussi avec réponse D-Bus vide ne produit plus de fausse erreur.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v13 puis v20 installés après essais RAM ; démarrage, relecture, réglages et validation du slot contrôlés.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Appel sortant réel arrivé à la sonnerie puis raccroché. Appels entrants et audio de conversation non validés.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Diagnostic caméra : un timeout n’est plus présenté comme une initialisation réussie. Deux capteurs détectés, sans trames reçues.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Caméra native indisponible, suspension CPU et commandes audio avancées incomplètes. Aucun nouvel APK.\n", + "en": "## In development — 27 September 2026\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Gaia Callscreen for incoming/outgoing calls, answer and hangup. Replaces the temporary v13 screen.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Persistent call log with acknowledgement and duplicate detection.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Distinguish unknown battery state from empty battery; tolerate missing or malformed Contacts indexes.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Render system text with Fira Sans while preserving icon fonts.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop with simulated modem: calls, failed hangup and retry, history, sleep/wake, Power menu, volume and battery.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: Power/Volume buttons connected to Gaia, with native screen, volume, shutdown and reboot controls.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: serialize brightness and power changes to prevent a black screen on wake; fix PulseAudio access for volume.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: successful hangup with an empty D-Bus reply no longer produces a false error.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v13 then v20 installed after RAM trials; startup, readback, settings and slot validation checked.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Real outgoing call reached ringing and was hung up. Incoming calls and conversation audio unvalidated.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Camera diagnostics: a timeout no longer counts as successful initialization. Two sensors detected, without received frames.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Native camera unavailable, CPU suspend and advanced audio controls incomplete. No new APK.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "development-2026-09-26-1", "text": { - "fr": "## En développement — 26 septembre 2026\n\n### Commun · Gaia\n\n- Polices : Fira Sans OpenType du simulateur historique, avec les mêmes fichiers\n pour les pages Gaia et le système Tundra. Correction du repli `sans-serif` natif\n et vérification Fontconfig ajoutée ; les polices d’icônes restent distinctes.\n- Paramètres : retrait des boutons de diagnostic « Paramètres Tundra » et\n « Tester une notification ». Le résumé des versions et les informations\n techniques restent disponibles.\n- Panneau matériel : transmission des capacités de la plateforme au panneau.\n Une variable absente provoquait une alerte Gecko au lieu d’afficher la batterie.\n\n### PC\n\n- Adaptateur local synchronisé avec les sources communes. Gecko reste en 156.0.1.\n\n### Android\n\n- Correctif du panneau présent dans les sources communes ; aucun nouvel APK construit.\n\n### Tundra / Sargo\n\n- Validation explicite du succès A/B après installation : sauvegarde et contrôle\n des métadonnées GPT, outil Qualcomm vérifié, confirmation Fastboot.\n- Erreurs du modem distinguées des erreurs de transport ; boîte d’erreur intégrée\n à Gaia. La caméra native indisponible ne suggère plus une autorisation inutile.\n- Diagnostic des polices réellement utilisées par Gecko et du réseau radio, sans\n appel ni SMS automatique. Fira Sans confirmée sur l’accueil et la barre d’état.\n- Installation réelle du v9 polices dans `boot_a` sur le Sargo inventorié.\n Contenu écrit relu et conforme ; cinq autres partitions de boot sauvegardées intactes.\n- Deux démarrages depuis le stockage : 10 contrôles Gaia et 9 contrôles système\n réussis par boot, réglages conservés. Le PC n’est plus nécessaire pour démarrer.\n Fichiers de secours Droidian conservés ; restauration réelle non testée.\n- Candidat persistant privé : image de données ext4 2 Gio, profils et réglages Gaia\n conservés, identité/comptes système préservés et suppression du watchdog de dix minutes.\n- Deux démarrages successifs de la même image v8 : 9 contrôles Gaia et 9 contrôles\n système réussis par boot. Diagnostic séparé : 15 contrôles avec la même interface.\n- Installation limitée à `boot_a` et restauration de l’original préparées et testées\n localement ; aucun flash effectué. Les limites matérielles restent documentées.\n- Démarrage normal sans ouverture automatique de Paramètres/Horloge ni défilement\n de l’accueil. Ces gestes appartiennent désormais au mode explicite `--diagnostics`.\n- Plein écran établi avant le chargement de Gaia ; décorations natives désactivées.\n Sur le Pixel testé : 360 × 740 pixels CSS, facteur 3, dimensions stables et accueil\n inchangé pendant les cinq relevés après démarrage.\n- v13 testé en RAM : huit contrôles de démarrage et neuf contrôles système réussis.\n Les rapports distinguent cette vérification du démarrage des tests interactifs.\n- Diagnostic v14 séparé : treize contrôles interactifs réussis, dont le panneau\n matériel ; ouverture et scan Wi-Fi vérifiés. Même interface que le v13 normal.\n- Les images de diagnostic interactif ne peuvent pas devenir la sélection normale\n du lanceur. L’ancienne sélection v13 est conservée dans son historique.\n- Modem enregistré en UMTS pendant l’essai ; lecture audio réussie et signal du\n microphone mesuré sans conserver d’enregistrement. Cela ne qualifie pas encore\n l’audio en appel, la caméra ou la veille.\n- Construction du ramdisk indépendante de la langue du PC : analyse `readelf` en\n locale C pour conserver le chargeur dynamique ARM64 et ses dépendances.\n\nLes six partitions boot/DTBO/vbmeta A/B sont inchangées. Le build reste un essai\nprivé en RAM, **pas une ROM flashable**. Détails : [état de Tundra](tundra/docs/STATUS.md).", - "en": "## In development — 26 September 2026\n\n### Shared · Gaia\n\n- Fonts: use the archived simulator’s Fira Sans OpenType files for both Gaia\n pages and Tundra system text. Fix native sans-serif fallback and add Fontconfig\n validation; icon fonts remain separate.\n- Settings: remove the “Tundra settings” and “Test a notification” diagnostic\n buttons. Release information and the technical details remain available.\n- Hardware panel: pass platform capabilities into the panel. An undefined variable\n used to open a Gecko alert instead of displaying battery information.\n\n### Desktop\n\n- Local adapter synchronized with the shared sources. Gecko remains at 156.0.1.\n\n### Android\n\n- Hardware panel fix is in the shared sources; no new APK has been built.\n\n### Tundra / Sargo\n\n- Explicit A/B boot-success finalization after installation: backed-up and checked\n GPT metadata, verified Qualcomm utility, Fastboot confirmation.\n- Modem rejections distinguished from transport failures; errors shown inside Gaia.\n The unavailable native camera no longer suggests an ineffective permission change.\n- Rendered-font and radio diagnostics without automatic calls or messages. Fira Sans\n confirmed on the home screen and status bar.\n- Installed the v9 font build into `boot_a` on the inventoried Sargo. Written\n image verified by readback; the other five backed-up boot partitions are unchanged.\n- Two boots from device storage pass 10 Gaia and 9 system checks each and retain\n settings. A PC is no longer required to boot. Droidian fallback files retained;\n actual restoration has not been tested.\n- Private persistent candidate: separate 2 GiB ext4 data image, retained Gaia\n settings/profiles and system identities, no ten-minute reboot watchdog.\n- Two consecutive boots of the same v8 image: 9 Gaia and 9 system checks pass on\n each boot. Separate diagnostic: 15 checks using identical shared UI files.\n- Boot-a-only installation and original-boot restoration tools prepared and tested\n locally. No actual flash performed; remaining hardware limits are documented.\n- Normal startup no longer opens Settings/Clock or scrolls the home screen. These\n actions now belong to the explicit `--diagnostics` mode.\n- Enter fullscreen before loading Gaia and hide native window decorations. On the\n tested Pixel: 360 × 740 CSS pixels at scale 3, stable dimensions and an unchanged\n home screen throughout five post-startup samples.\n- v13 RAM trial passes eight startup checks and nine system checks. Reports now\n distinguish startup observation from interactive qualification.\n- Separate v14 diagnostic: thirteen interactive checks pass, including the hardware\n panel; Wi-Fi panel and scanning verified with the same UI files as normal v13.\n- Interactive diagnostic images are rejected as the normal launch target. The\n earlier v13 selection is retained in the launcher history.\n- Modem registered on UMTS during the trial; audio playback completed and microphone\n samples were measured without retaining a recording. In-call audio, camera and\n suspend remain unqualified.\n- Ramdisk dependency discovery now runs `readelf` in the C locale, preserving the\n ARM64 dynamic loader regardless of the build machine's language.\n\nAll six A/B boot/DTBO/vbmeta partitions are unchanged. This remains a private RAM\ntrial, **not a flashable ROM**. See [Tundra status](tundra/docs/STATUS.md)." + "fr": "## En développement — 26 septembre 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Polices Fira Sans historiques utilisées dans Gaia et Tundra ; repli sans-serif et contrôle Fontconfig corrigés.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Boutons de diagnostic retirés des Paramètres.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Panneau matériel : capacités de la plateforme transmises correctement pour afficher la batterie.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : erreurs modem et transport distinguées ; message caméra indisponible corrigé.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : données persistantes sur ext4 2 Gio et validation du succès A/B après installation.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v8 : deux boots avec persistance, 9 contrôles Gaia et 9 système par boot. Outils d’installation et de secours testés localement.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v9 installé sur boot_a : deux démarrages avec 10 contrôles Gaia et 9 système chacun. Relecture conforme, cinq autres partitions préservées ; restauration réelle non testée.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Démarrage normal sans gestes de diagnostic automatiques ; plein écran établi avant Gaia, dimensions stables à 360 × 740 pixels CSS.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Mode `--diagnostics` séparé ; images de diagnostic exclues du lancement normal.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : v13 testé en RAM (8 contrôles de démarrage, 9 système), v14 en diagnostic (13 contrôles interactifs), Wi-Fi inclus.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Enregistrement UMTS, lecture audio et signal du micro vérifiés. Audio d’appel, caméra et veille non validés.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Construction du ramdisk indépendante de la langue du PC grâce à readelf en locale C.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Essais privés, sans ROM publique. Sources PC synchronisées ; correctif commun disponible pour Android, sans nouvel APK.\n", + "en": "## In development — 26 September 2026\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Use historical Fira Sans fonts in Gaia and Tundra; correct sans-serif fallback and Fontconfig checks.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Removed diagnostic buttons from Settings.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Hardware panel: pass platform capabilities correctly to display the battery.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: distinguish modem and transport errors; correct the unavailable-camera message.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: persistent data on a 2 GiB ext4 image and A/B success validation after installation.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v8: two boots with persistence, 9 Gaia and 9 system checks per boot. Installation and recovery tools tested locally.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v9 installed on boot_a: two boots with 10 Gaia and 9 system checks each. Matching readback, five other partitions preserved; actual restoration untested.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Normal startup without automatic diagnostic gestures; fullscreen before Gaia, stable 360 × 740 CSS-pixel layout.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Separate `--diagnostics` mode; diagnostic images excluded from normal launch.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: v13 tested in RAM (8 startup, 9 system checks), v14 diagnostics (13 interactive checks), including Wi-Fi.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) UMTS registration, audio playback and microphone signal checked. Call audio, camera and suspend unvalidated.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Ramdisk builds independent of the desktop language by running readelf in the C locale.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Private trials, without a public ROM. Desktop sources synchronized; shared Android fix available, without a new APK.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "development-2026-09-25-1", "text": { - "fr": "## En développement — 25 septembre 2026\n\n### Commun · Gaia\n\n- **Navigation :** bouton Accueil rond ; dégradé noir vers transparent au-dessus du\n fond d’écran, fond bleu uni dans les applications. L’appui modifie le cercle\n plutôt que de laisser un rectangle bleu sur l’accueil.\n- **Défilement :** le dégradé disparaît au bas du panneau de l’accueil ; il revient\n quand on remonte. Le changement de panneau Applications / Pages est pris en compte.\n- **Typographie :** polices Fira Sans embarquées, avec plusieurs graisses et italiques,\n au lieu de dépendre des polices installées sur le système hôte.\n- **Informations :** affichage séparé de Vulpes, Gaia, du moteur réellement exécuté,\n de la plateforme, du modèle et de l’adaptateur. Le panneau prévoit Tundra et\n l’adaptation matérielle, maintenant raccordés au backend natif Tundra.\n- **Icônes :** reprise du chargement après une première mesure de taille nulle,\n partagée entre PC, Android et Tundra. Le correctif était auparavant limité à Android.\n- **Maintenance :** ce journal centralise les évolutions des trois plateformes.\n\n### PC · Validé localement\n\n- Chargement d’une icône initialisée à taille nulle puis rendue visible : testé.\n- Navigation Accueil / Paramètres / retour Accueil, fond transparent en bas de page,\n dégradé en haut et contrôle des permissions testés dans un profil isolé.\n- Panneau Informations testé en français et en anglais, sans anciennes lignes en double.\n- Le moteur demeure Gecko **156.0.1** ; ces changements d’interface ne recompilent pas Gecko.\n\n### Android · Sources adaptées, Java compilé\n\n- Même comportement de navigation prévu par le pont GeckoView / Android : barre\n superposée sur l’accueil, place réservée dans les applications et le navigateur.\n- État de navigation conservé lors de la recréation de l’activité Android.\n- Compilation Java réussie. **Aucun nouvel APK livré pour ces changements** et\n comportement visuel encore à retester sur Android.\n\n### Tundra / Sargo · Essais privés en RAM\n\n- Build réseau v6 devenu référence du lanceur après 13 contrôles Gaia et 9\n contrôles système ; retour automatique et six partitions inchangées vérifiés.\n- Backend Tundra distinct du PC : modèle Pixel 3a, adaptation Sargo et version\n native affichés sans annoncer une caméra ou une téléphonie non raccordées.\n- Batterie et charge lues sur le matériel ; panneau d’état bilingue dans Gaia.\n 13 contrôles Gaia réussis sur l’essai réseau, dont ce nouveau panneau.\n- Wi-Fi réel testé : scan, connexion, IPv4, DNS et page HTTPS sous Gecko.\n Ajout de `pd-mapper`, désactivation d’IPA non initialisé et du sondage caméra\n prématuré ; 9 contrôles système passent, sans Oops noyau. La sélection du réseau\n dans Gaia reste à raccorder ; les identifiants du test restent en RAM.\n- Configuration des services embarquable dans le petit ramdisk : pas de nouvelle\n copie des 3 Gio pour chaque ajustement de démarrage. Partitions modem protégées\n en utilisant des copies NV en RAM pour les essais Qualcomm.\n\n- Nouvelle image système ext4 contenant le runtime ARM64, les HAL de référence,\n Phoc, Gecko et Gaia. Le runtime n’est plus chargé depuis l’arborescence de travail\n Droidian, ni le HAL depuis son ancien fichier `/var/lib/lxc/android/`.\n- Démarrage du second essai vérifié sur Pixel 3a : PID 1 Tundra, services HAL,\n compositeur et Gaia ; **7 contrôles Gaia et 8 contrôles système réussis**.\n- Capture physique de l’accueil contrôlée : icônes présentes, Fira Sans chargée,\n bouton rond et dégradé visibles. Gecko **156.0.1 ARM64**, WebRender matériel.\n- Petits changements d’interface embarqués dans le ramdisk d’essai : inutile de\n retransférer les 3 Gio du système pour chaque correction CSS/JavaScript.\n- Copie des sources de construction conservée avec les nouvelles images, vérifiée\n par empreintes, pour permettre de les retester après l’évolution du code courant.\n- Le dernier essai (`sargo-system-boot-20260925-v5`) valide **10 contrôles Gaia et\n 8 contrôles système**, dont icônes, police chargée, fond à l’appui, bas de page\n et retour depuis une application. Capture physique contrôlée ; référence locale\n du lanceur mise à jour. Aucun APK publié ni téléphone flashé.\n\n**Limites :** stockage installé encore utilisé comme support du fichier système ;\nmodifications perdues à l’arrêt ; retour automatique au système conservé après dix\nminutes. Aucun flash ni remplacement de Droidian. Le redémarrage à chaud du\ncompositeur a échoué lors d’un essai ; le démarrage à froid fonctionne. Réseau,\nmodem, audio, caméra et veille restent à intégrer ou qualifier. Ce n’est pas une\nROM publique installable ni un téléphone quotidien validé.\n\nDétails : [image système](tundra/docs/SYSTEM-IMAGE-SARGO.md),\n[état Tundra](tundra/docs/STATUS.md), [parité des plateformes](docs/PARITE-BUREAU-ANDROID.md).", - "en": "## In development — 25 September 2026\n\n### Shared · Gaia\n\n- **Navigation:** a circular Home button, a black-to-transparent gradient over the wallpaper,\n and a solid blue bar inside apps. Pressing Home now changes the ring instead of leaving\n a blue rectangle over the home screen.\n- **Scrolling:** the gradient disappears at the bottom of the home panel and returns when\n scrolling up. Switching between the Apps and Pages panels is supported.\n- **Typography:** bundled Fira Sans fonts, with multiple weights and italics, instead of\n relying on the host system’s installed fonts.\n- **Device information:** separate product, Gaia, actual engine, platform, model and adapter\n versions. The panel includes support for Tundra and hardware adaptation fields;\n connecting these to the native platform is still pending.\n- **Icons:** loading resumes after an initial zero-width measurement, across desktop,\n Android and Tundra. This fix was previously limited to Android.\n- **Maintenance:** this changelog brings together the three platforms’ progress.\n\n### Desktop · Locally tested\n\n- Tested an icon initially measured at zero width, then made visible.\n- Tested Home / Settings / Home navigation, a transparent bar at the bottom, the gradient\n at the top and permission checks in an isolated profile.\n- Tested the information panel in French and English, without duplicate legacy rows.\n- The engine remains Gecko **156.0.1**; these UI changes do not rebuild Gecko.\n\n### Android · Sources adapted, Java compiled\n\n- The GeckoView / Android bridge implements the same navigation states: an overlay on Home,\n with space reserved for the bar in apps and the browser.\n- Navigation state survives Android activity recreation.\n- Java compilation passed. **No new APK delivered for these changes**;\n visual behaviour still needs testing on Android.\n\n### Tundra / Sargo · Private RAM trials\n\n- Network build v6 is the launcher reference after 13 Gaia and 9 system checks;\n automatic return and unchanged hashes of all six boot partitions verified.\n- Separate Tundra backend reports Pixel 3a, Sargo adaptation and native version,\n without claiming an unimplemented camera or telephony stack.\n- Real battery and charging state with a bilingual Gaia hardware panel.\n The network trial passed 13 Gaia checks, including this panel.\n- Real Wi-Fi tested: scan, connection, IPv4, DNS and HTTPS in Gecko. Added\n `pd-mapper`, disabled uninitialized IPA offload and premature camera probing;\n 9 system checks pass without kernel Oops. Gaia network selection is still\n pending; trial credentials remain in RAM.\n- Service configuration can travel in the small ramdisk, avoiding another 3 GiB\n transfer per boot adjustment. Qualcomm trials use RAM copies of modem NV data.\n\n- A new ext4 system image contains the ARM64 runtime, reference HALs, Phoc, Gecko and Gaia.\n The runtime is no longer loaded from the Droidian working directory, nor the HAL\n from its previous `/var/lib/lxc/android/` location.\n- The second Pixel 3a trial passed **7 Gaia checks and 8 system checks**, with Tundra PID 1,\n its HAL services, compositor and Gaia.\n- A physical display capture confirms home icons, loaded Fira Sans, the circular button\n and gradient. Gecko **156.0.1 ARM64**, with hardware WebRender.\n- Small UI changes travel in the trial ramdisk, avoiding another 3 GiB system transfer\n for each CSS or JavaScript adjustment.\n- Build sources are saved alongside new images and verified by hashes, so qualified\n images remain testable after the checkout changes.\n- The latest trial (`sargo-system-boot-20260925-v5`) passes **10 Gaia checks and 8 system\n checks**, including icons, fonts, pressed Home, bottom-of-page behaviour and app return.\n Physical capture reviewed; local launcher reference updated. No APK published or phone flashed.\n\n**Limitations:** the installed filesystem still carries the system image; writes are lost\nat shutdown; the trial returns to the retained system after ten minutes. No flashing or\nDroidian replacement. A warm compositor restart failed during one trial; cold boot works.\nNetworking, modem, audio, camera and suspend still need integration or qualification.\nThis is not a public installable ROM or a validated daily-use phone." + "fr": "## En développement — 25 septembre 2026\n\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Bouton Accueil rond, dégradé transparent sur le fond d’écran et barre bleue dans les applications.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Dégradé masqué en bas de page et rétabli au défilement ; plus de rectangle bleu à l’appui sur Accueil.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Fira Sans embarquée avec graisses et italiques ; versions produit, Gaia, moteur et plateforme affichées séparément.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Reprise du chargement des icônes après une première mesure de taille nulle sur les trois plateformes.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) PC : icônes, navigation, permissions et panneau Informations FR/EN vérifiés avec Gecko 156.0.1.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android : état de navigation conservé après recréation de l’activité. Compilation Java réussie ; rendu à retester, aucun nouvel APK livré.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Tundra : image ext4 avec runtime ARM64, HAL, Phoc, Gecko et Gaia ; petits correctifs embarqués dans le ramdisk.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : backend dédié, informations Pixel 3a et lecture matérielle de la batterie. Sources de construction archivées avec empreintes.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo : WebRender matériel, icônes et polices contrôlés sur capture physique. v5 : 10 contrôles Gaia et 8 système réussis.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo : ajout de pd-mapper, désactivation d’IPA non initialisé et du sondage caméra prématuré. Données NV du modem copiées en RAM pour les essais.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v6 : scan Wi-Fi, connexion, IPv4, DNS et HTTPS vérifiés ; 13 contrôles Gaia et 9 système réussis. Sélection du réseau dans Gaia encore à raccorder.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Essais RAM sans flash, retour automatique après dix minutes et modifications perdues à l’arrêt. Démarrage à froid réussi ; redémarrage à chaud du compositeur en échec.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Pas de ROM publique ; modem, audio, caméra et veille encore à intégrer ou valider.\n", + "en": "## In development — 25 September 2026\n\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Round Home button, transparent wallpaper gradient and blue bar inside apps.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Hide the gradient at the bottom and restore it when scrolling; remove the blue rectangle when pressing Home.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Bundled Fira Sans weights and italics; separate product, Gaia, engine and platform versions.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Resume icon loading after an initial zero-size measurement on all three platforms.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Desktop: icons, navigation, permissions and French/English information panel checked with Gecko 156.0.1.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android: retain navigation state after activity recreation. Java compilation passed; visuals need retesting, no new APK delivered.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Tundra: ext4 image with ARM64 runtime, HAL, Phoc, Gecko and Gaia; small fixes bundled in the ramdisk.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: dedicated backend, Pixel 3a information and hardware battery readings. Build sources archived with checksums.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo: hardware WebRender, icons and fonts checked on a physical display capture. v5: 10 Gaia and 8 system checks passed.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Sargo: add pd-mapper, disable uninitialized IPA and premature camera probing. Copy modem NV data to RAM for trials.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Sargo v6: Wi-Fi scan, connection, IPv4, DNS and HTTPS verified; 13 Gaia and 9 system checks passed. Gaia network selection still pending.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) RAM trials without flashing, automatic return after ten minutes and changes lost at shutdown. Cold boot passed; warm compositor restart failed.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) No public ROM; modem, audio, camera and suspend still need integration or validation.\n" + }, + "metadata": { + "kind": "development", + "status": "development", + "platforms": [ + "pc", + "apk", + "models" + ], + "models": [ + "sargo" + ] } }, { "id": "2.7-preview.7", "text": { - "fr": "## 2.7-preview.7 — APK construit, référence locale\n\n### Commun / PC / Android\n\n- Correction de l’autorisation interne d’ouverture du capteur sur PC et de la\n reprise après la première demande de permission CAMERA sur Android.\n- Messages Photo distincts pour un refus, un capteur absent ou un échec d’ouverture.\n- Version produit commune **2.7-preview.7**, Gaia **2.7.0** ; numéros du moteur séparés.\n- Gecko PC **156.0.1**, GeckoView Android **156.0** : l’écart réel de correctif est\n affiché, sans renommer artificiellement l’un des moteurs.\n\nDétails : [Photo et Galerie](docs/CAMERA-GALERIE.md), [README Android](android/README.md).", - "en": "## 2.7-preview.7 — APK built, local reference\n\n### Shared / Desktop / Android\n\n- Fixed internal camera allocation permission on desktop and recovery after the first\n CAMERA permission prompt on Android.\n- Photo distinguishes permission denial, a missing sensor and an opening failure.\n- Shared product version **2.7-preview.7**, Gaia **2.7.0**, with separate engine versions.\n- Desktop Gecko **156.0.1**, Android GeckoView **156.0**: the real patch difference is\n reported rather than relabelling either engine." + "fr": "## 2.7-preview.7 — APK construit, référence locale\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Permission caméra sur PC et reprise après la première autorisation Android.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Messages distincts pour une permission refusée, un capteur absent et un échec d’ouverture.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Versions produit et moteur séparées : Vulpes Preview7, Gaia 2.7.0, Gecko PC 156.0.1 et GeckoView Android 156.0.\n", + "en": "## 2.7-preview.7 — APK built, local reference\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Desktop camera permission and recovery after the first Android permission prompt.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Separate messages for denied permission, missing sensors and opening failures.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Separate product and engine versions: Vulpes Preview7, Gaia 2.7.0, desktop Gecko 156.0.1 and Android GeckoView 156.0.\n" + }, + "metadata": { + "kind": "version", + "status": "built", + "platforms": [ + "pc", + "apk" + ], + "models": [] } }, { "id": "2.7-preview.6", "text": { - "fr": "## 2.7-preview.6 — APK construit\n\n### Commun / PC / Android\n\n- Portage de la véritable application Photo Gaia : aperçu et capture JPEG via\n webcam sur PC ou capteur via GeckoView sur Android.\n- Photos enregistrées dans Galerie Gaia ; import de médias par le sélecteur du système.\n- Commandes non prises en charge désactivées : notamment flash et vidéo ; changement\n de caméra selon les capteurs disponibles.", - "en": "## 2.7-preview.6 — APK built\n\n### Shared / Desktop / Android\n\n- Ported the original Gaia Camera app: webcam preview and JPEG capture on desktop,\n or capture through GeckoView on Android.\n- Photos saved in Gaia Gallery; media imports use the host system’s file picker.\n- Unsupported controls disabled, including flash and video; camera switching depends\n on available sensors." + "fr": "## 2.7-preview.6 — APK construit\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Photo Gaia sur PC et Android : aperçu et capture JPEG via webcam ou GeckoView.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Enregistrement dans Galerie et import par le sélecteur du système.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Flash et vidéo désactivés ; changement de caméra selon les capteurs disponibles.\n", + "en": "## 2.7-preview.6 — APK built\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Gaia Camera on desktop and Android: preview and JPEG capture through webcam or GeckoView.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Save photos in Gallery and import through the system picker.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Flash and video disabled; camera switching depends on available sensors.\n" + }, + "metadata": { + "kind": "version", + "status": "built", + "platforms": [ + "pc", + "apk" + ], + "models": [] } }, { "id": "2.7-preview.5", "text": { - "fr": "## 2.7-preview.5 — APK construit\n\n### Commun\n\n- Versions centralisées dans `release.json`, séparation produit / Gaia / moteur.\n- Interface des fonctions natives partagée entre PC et Android ; contrôle du contenu\n commun réellement embarqué dans l’APK avant distribution.\n\n### PC\n\n- Intégration des panneaux de réglages Linux disponibles, import de médias et vCard,\n notifications du bureau et capture webcam de test.\n\n### Android\n\n- Même interface Gaia et commandes natives que le PC, adaptées aux capacités Android.\n- Tests de retour au premier plan et de récupération après arrêt du processus de rendu.", - "en": "## 2.7-preview.5 — APK built\n\n### Shared\n\n- Centralised versions in `release.json`, separating product, Gaia and engine versions.\n- Shared native integration UI between desktop and Android; packaged common files\n checked inside the APK before distribution.\n\n### Desktop\n\n- Integration with available Linux settings panels, media and vCard imports,\n desktop notifications and a test webcam capture.\n\n### Android\n\n- Same Gaia interface and native commands as desktop, adapted to Android capabilities.\n- Foreground return and recovery after rendering-process termination tested." + "fr": "## 2.7-preview.5 — APK construit\n\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Versions centralisées dans `release.json` ; produit, Gaia et moteur séparés.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Interface native commune PC/Android et contrôle de parité des ressources de l’APK.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) PC : réglages Linux, imports médias/vCard, notifications et capture webcam de test.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Android : retour au premier plan et récupération après arrêt du processus de rendu.\n", + "en": "## 2.7-preview.5 — APK built\n\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Versions centralized in `release.json`; product, Gaia and engine separated.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Shared desktop/Android native interface and APK asset parity checks.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Desktop: Linux settings, media/vCard imports, notifications and test webcam capture.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Android: foreground return and recovery after renderer termination.\n" + }, + "metadata": { + "kind": "version", + "status": "built", + "platforms": [ + "pc", + "apk" + ], + "models": [] } }, { "id": "2.7-156.0.2", "text": { - "fr": "## 2.7-156.0.2 — APK historique construit (révision 4)\n\nCe nom historique mélangeait révision du produit et version du moteur ; il ne\nsignifie pas que l’APK utilisait Gecko 156.0.2. Le moteur Android était Gecko 156.0.\n\n### Android\n\n- Reprise de l’activité et des sessions pour limiter les écrans blancs au retour dans Vulpes.\n- Accès aux réglages Android, au volume et à la luminosité de la fenêtre.\n- Import de médias et contacts, notifications natives avec permissions.\n- Transfert au composeur, à SMS et à Horloge Android ; aucun envoi silencieux ni\n remplacement des applications système Téléphone/SMS.\n- Capture initialement déléguée à l’application photo Android, avant le portage de Photo Gaia.", - "en": "## 2.7-156.0.2 — Historical APK built (revision 4)\n\nThis historical name mixed product revision and engine version; it does not mean\nthat the APK used Gecko 156.0.2. Its Android engine was Gecko 156.0.\n\n### Android\n\n- Activity and session recovery to reduce white screens when returning to Vulpes.\n- Access to Android settings, volume and window brightness.\n- Media and contact imports, native notifications with permissions.\n- Handoff to Android’s dialer, SMS and clock apps; no silent sending or replacement\n of the system Phone and SMS apps.\n- Capture initially delegated to Android’s camera app, before the Gaia Camera port." + "fr": "## 2.7-156.0.2 — APK historique construit (révision 4)\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android : reprise de l’activité et des sessions pour réduire les écrans blancs.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Accès aux réglages Android, volume et luminosité de la fenêtre.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Import de médias et contacts ; notifications natives avec permissions.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Transfert vers Téléphone, SMS, Horloge et Appareil photo Android. Aucun envoi silencieux ni remplacement des applications par défaut.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Numérotation historique : moteur Android Gecko 156.0, malgré le nom 2.7-156.0.2.\n", + "en": "## 2.7-156.0.2 — Historical APK built (revision 4)\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Android: activity and session recovery to reduce blank screens.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Access to Android settings, volume and window brightness.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Media and contact imports; native notifications with permissions.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Handoff to Android Phone, SMS, Clock and Camera. No silent sending or replacement of default apps.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Historical numbering: Android engine Gecko 156.0, despite the 2.7-156.0.2 name.\n" + }, + "metadata": { + "kind": "version", + "status": "built", + "platforms": [ + "apk" + ], + "models": [] } }, { "id": "2.7-156.0.1", "text": { - "fr": "## 2.7-156.0.1 — APK publié (révision 3)\n\n- Nom **Vulpes OS - Preview** et icône du projet.\n- Correctif du fond d’écran clignotant inclus.\n- Numérotation historique conservée ; ne pas en déduire le patch du moteur Android.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1)", - "en": "## 2.7-156.0.1 — APK published (revision 3)\n\n- **Vulpes OS - Preview** name and project icon.\n- Wallpaper flickering fix included.\n- Historical numbering retained; it does not identify the Android engine’s patch version.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1)" + "fr": "## 2.7-156.0.1 — APK publié (révision 3)\n\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Nom « Vulpes OS - Preview » et icône du projet.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Clignotement du fond d’écran corrigé.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Numérotation historique, distincte de la version du moteur Android.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1)\n", + "en": "## 2.7-156.0.1 — APK published (revision 3)\n\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) “Vulpes OS - Preview” name and project icon.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Wallpaper flickering corrected.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Historical numbering, separate from the Android engine version.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1)\n" + }, + "metadata": { + "kind": "version", + "status": "published", + "platforms": [ + "apk" + ], + "models": [] } }, { "id": "2.7-preview.2", "text": { - "fr": "## 2.7-preview.2 — APK publié\n\n- Correction du fond d’écran clignotant dans la Preview Android.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.2)", - "en": "## 2.7-preview.2 — APK published\n\n- Fixed wallpaper flickering in the Android Preview.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.2)" + "fr": "## 2.7-preview.2 — APK publié\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Clignotement du fond d’écran dans la Preview Android.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.2)\n", + "en": "## 2.7-preview.2 — APK published\n\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Wallpaper flickering in the Android Preview.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.2)\n" + }, + "metadata": { + "kind": "version", + "status": "published", + "platforms": [ + "apk" + ], + "models": [] } }, { "id": "2.7-preview.1", "text": { - "fr": "## 2.7-preview.1 — APK publié\n\n- Première Preview Android embarquant Gaia et GeckoView.\n- Version archivée ; clignotement du fond d’écran connu.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.1)", - "en": "## 2.7-preview.1 — APK published\n\n- First Android Preview bundling Gaia and GeckoView.\n- Archived version; known wallpaper flickering issue.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.1)" + "fr": "## 2.7-preview.1 — APK publié\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Première Preview Android avec Gaia et GeckoView.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Version archivée ; clignotement du fond d’écran connu.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.1)\n", + "en": "## 2.7-preview.1 — APK published\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) First Android Preview with Gaia and GeckoView.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Archived version; known wallpaper flickering issue.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.1)\n" + }, + "metadata": { + "kind": "version", + "status": "published", + "platforms": [ + "apk" + ], + "models": [] } }, { "id": "history", "text": { - "fr": "## Étapes antérieures — rétrospective technique\n\nCes étapes ne correspondent pas à de nouvelles releases publiques datées.\n\n### Gaia / PC\n\n- Constitution de Vulpes à partir de Gaia historique ; identité visuelle, fond\n d’écran, traductions et travail sur les informations système.\n- Portage expérimental Gecko 45 → 52, puis séparation de Gaia, des services et du\n moteur pour l’hôte Gecko récent.\n- Réimplémentation des API nécessaires : réglages, registre d’applications,\n contacts locaux, messages, alarmes, stockage et communications entre applications.\n- Adaptation des composants visuels anciens, des interactions tactiles simulées,\n du navigateur et des applications ; essais d’applications Firefox OS archivées.\n- Outillage de mise à jour Gecko avec vérifications, moteur candidat et retour\n arrière explicite. Il détecte les incompatibilités, sans les réparer automatiquement.\n- Lancement et mise à jour de Gaia sans recompilation complète du moteur.\n\n### Tundra / Sargo\n\n- Inventaire du Pixel 3a, sauvegardes des partitions de boot et tests USB de diagnostic.\n- Tests QEMU, puis Gecko ARM64 dans Droidian et sous un compositeur dédié.\n- Racine distincte, services HAL sélectionnés et PID 1 Tundra ; essais graphiques\n temporaires par Fastboot avec secours SSH et retour automatique.\n- Noyau de référence **4.9-124-google-sargo**, adaptation **Android 9 / API 28**.", - "en": "## Earlier milestones — technical retrospective\n\nThese milestones are not newly dated public releases.\n\n### Gaia / Desktop\n\n- Vulpes assembled from historical Gaia: branding, wallpaper, translations and\n work on system information.\n- Experimental Gecko 45 → 52 port, followed by separation of Gaia, services and\n the engine for the current Gecko host.\n- Reimplementation of required APIs: settings, application registry, local contacts,\n messages, alarms, storage and inter-app communication.\n- Adaptation of old UI components, simulated touch, browser and applications;\n testing of archived Firefox OS apps.\n- Gecko update tools with checks, candidate engines and explicit rollback.\n They detect incompatibilities without automatically repairing them.\n- Gaia launch and updates without rebuilding the whole engine.\n\n### Tundra / Sargo\n\n- Pixel 3a inventory, boot partition backups and USB diagnostic trials.\n- QEMU trials, then ARM64 Gecko inside Droidian and under a dedicated compositor.\n- Separate root, selected HAL services and Tundra PID 1; temporary graphical\n Fastboot trials with rescue SSH and automatic return.\n- Reference kernel **4.9-124-google-sargo**, **Android 9 / API 28** adaptation." + "fr": "## Étapes antérieures — rétrospective technique\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Base Gaia historique avec identité Vulpes, fond d’écran, traductions et informations système.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Portage expérimental Gecko 45 vers 52, puis séparation de Gaia, des services et du moteur.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) API de réglages, applications, contacts, messages, alarmes, stockage et communications entre applications.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Adaptation des composants Gaia, gestes tactiles, navigateur et applications aux moteurs récents.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Mise à jour Gecko avec contrôles et retour arrière ; mise à jour Gaia sans recompilation du moteur.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Applications Firefox OS archivées, QEMU, puis Gecko ARM64 dans Droidian et sous compositeur dédié.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo : inventaire, sauvegardes de boot, racine distincte, services HAL et PID 1 Tundra.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Essais graphiques Fastboot temporaires avec secours SSH et retour automatique.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Noyau 4.9-124-google-sargo, adaptation Android 9/API 28. Étapes historiques, sans nouvelles releases datées.\n", + "en": "## Earlier milestones — technical retrospective\n\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Historical Gaia base with Vulpes branding, wallpaper, translations and system information.\n- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) Experimental Gecko 45 to 52 port, followed by separation of Gaia, services and engine.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) APIs for settings, apps, contacts, messages, alarms, storage and inter-app communication.\n- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) Adapt Gaia components, touch gestures, browser and apps to recent engines.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Gecko updates with checks and rollback; Gaia updates without rebuilding the engine.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Archived Firefox OS apps, QEMU, then ARM64 Gecko inside Droidian and under a dedicated compositor.\n- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) Sargo: inventory, boot backups, separate root, HAL services and Tundra PID 1.\n- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) Temporary graphical Fastboot trials with SSH recovery and automatic return.\n- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) Kernel 4.9-124-google-sargo, Android 9/API 28 adaptation. Historical milestones, without newly dated releases.\n" + }, + "metadata": { + "kind": "history", + "status": "history", + "platforms": [ + "pc", + "models" + ], + "models": [ + "sargo" + ] } } ], @@ -254,6 +888,12 @@ "after": "2.7-preview.15", "url": "https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.17", "summary": "# Français\n\nAPK expérimental Android : Vulpes OS 2.7-preview.17, Gaia 2.7.0, GeckoView 156.0. Installation par-dessus une Preview précédente signée avec la même clé. Android 8.0/API26 minimum ; essais réalisés sur émulateur Android 16/API36.\n\n## 2.7-preview.17 — APK construit · 5 octobre 2026\n\n### Commun · Gaia / PC\n\n- Remettre Marketplace sur l’accueil avec son icône originale et le catalogue Vulpes. Ouvrir sa version française pour les langues fr, anglaise sinon.\n- Adapter les téléchargements du catalogue en installations confirmées : vérifier les archives ZIP, ajouter l’application à l’accueil et permettre son lancement et sa désinstallation.\n- Conserver les applications installées séparément des sources Gaia, sur des origines distinctes et sans privilèges système hérités des anciens manifestes.\n- Valider sur PC le parcours réel avec 2048 : catalogue, confirmation, installation, lancement, persistance et désinstallation. Contrôler les archives corrompues, chemins invalides, limites, annulation et confirmations à usage unique.\n\n### Android · APK\n\n- Construire et signer Preview17 avec le même Marketplace et le même service d’installation. Utiliser la vue GeckoView existante pour le catalogue et le stockage privé Android pour les paquets.\n- Vérifier sur émulateur Android 16/API36 le clic Installer, la confirmation, l’apparition sur l’accueil et le lancement de 2048 sans accès au pont système Vulpes. Vérifier aussi la conservation après arrêt forcé et relance, puis la désinstallation.\n\n## 2.7-preview.16 — APK construit · 5 octobre 2026\n\n### Commun · Gaia / PC\n\n- Rétablir les sélecteurs Gaia de fond d’écran et de sons, leur retour vers Paramètres et leur annulation par Accueil. Partager le transport des images et sons enregistrés entre PC et Android.\n- Propager les changements de langue à l’accueil, y compris sans permission de lecture générale des réglages. Vérifier les libellés FR/EN et la persistance.\n- Rétablir l’arrêt du balayage horizontal sur une page et masquer les applications de développement sur l’accueil, sans les supprimer des sources.\n- Utiliser le logo Vulpes fourni dans le splash et adapter les composants originaux de la liste des sons au moteur actuel.\n- Vérifier sur PC deux changements consécutifs de fond, le choix d’un son et les panneaux Gaia restaurés.\n\n### Android · APK\n\n- APK local Preview16 construit et signé ; contenu commun et version vérifiés. Aucune publication dans cette étape.\n- Valider Vulpes comme lanceur HOME, afficher le verrouillage Gaia après veille et rétablir le retour Android depuis la caméra ouverte au verrouillage.\n- Masquer les barres système Android en mode immersif, en conservant leur accès temporaire par balayage.\n- Tests sur émulateur Android 16/API36 : verrouillage, caméra/retour, FR/EN, accueil sans applications de test, fond d’écran et son. Fond conservé après arrêt forcé et relance, sans changement répété pendant 15 secondes. Autres appareils Android non qualifiés dans cette étape.\n\nLes correctifs caméra du 8 octobre ainsi que les derniers correctifs Wi-Fi, bannière et thème Marketplace du 6 octobre ne sont pas inclus dans cet APK. La validation de la caméra avant sur Sargo reste ouverte. Cette publication concerne uniquement Android.\n\n# English\n\nExperimental Android APK: Vulpes OS 2.7-preview.17, Gaia 2.7.0, GeckoView 156.0. Install over an earlier Preview signed with the same key. Android 8.0/API26 minimum; tested on an Android 16/API36 emulator.\n\n## 2.7-preview.17 — APK built · 5 October 2026\n\n### Shared · Gaia / Desktop\n\n- Restore Marketplace on Home with its original icon and the Vulpes catalogue. Select French for fr languages and English otherwise.\n- Turn catalogue downloads into confirmed installations: validate ZIP archives, add the application to Home and support launch and removal.\n- Store installed applications separately from Gaia sources, on distinct origins and without system privileges inherited from archived manifests.\n- Validate the real desktop workflow with 2048: catalogue, confirmation, installation, launch, persistence and removal. Check damaged archives, invalid paths, limits, cancellation and single-use confirmations.\n\n### Android · APK\n\n- Build and sign Preview17 with the same Marketplace and installation service. Use the existing GeckoView browser for the catalogue and private Android storage for packages.\n- Verify the Install click, confirmation, Home icon and 2048 launch without access to the Vulpes system bridge on an Android 16/API36 emulator. Also verify persistence after force-stop/relaunch and removal.\n\n## 2.7-preview.16 — APK built · 5 October 2026\n\n### Shared · Gaia / Desktop\n\n- Restore Gaia wallpaper and sound pickers, return navigation to Settings and cancellation through Home. Share persisted image and sound transport between desktop and Android.\n- Propagate language changes to Home without granting general settings read access. Check French/English labels and persistence.\n- Restore horizontal page snapping and hide development apps from Home without deleting their sources.\n- Use the supplied Vulpes logo in the splash and adapt the original sound-list components to the current engine.\n- Verify two consecutive wallpaper changes, sound selection and restored Gaia panels on desktop.\n\n### Android · APK\n\n- Build and sign local Preview16; verify shared payload and version. Nothing published in this step.\n- Validate Vulpes as the HOME launcher, restore Gaia locking after sleep and Android Back navigation from the lockscreen camera.\n- Hide Android system bars in immersive mode while retaining temporary swipe access.\n- Android 16/API36 emulator checks: locking, camera/Back, French/English, Home without test apps, wallpaper and sound. Wallpaper retained after force-stop/relaunch with no repeated changes over 15 seconds. Other Android devices not qualified in this step.\n\nThe October 8 camera changes and the later October 6 Wi-Fi, banner and Marketplace theme changes are not included in this APK. Front-camera validation on Sargo remains open. This release concerns Android only." + }, + "v2.7-preview.20": { + "version": "2.7-preview.20", + "after": "2.7-preview.17", + "url": "https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.20", + "summary": "# 🇫🇷 Vulpes OS — Preview 20\n\nAPK expérimental et sources communes PC / Android / Tundra mis à jour depuis la Preview 17. Les étapes 18 et 19 sont incluses dans cette version ; aucun ancien APK supplémentaire n’est publié.\n\n- Android 8 / API 26 minimum déclaré, API 37 ciblée. ARM64, ARMv7 et x86_64. Installer l’APK par-dessus une Preview signée avec la même clé conserve ses données.\n- Vérifier le téléchargement avec `sha256sum -c SHA256SUMS.txt`.\n- PC : `./setup.sh`, puis `./run.sh`. Sources Sargo incluses ; cette release ne distribue pas de nouvelle image native.\n- Vulpes 2.7-preview.20 · Gaia 2.7.0 · Gecko PC/Sargo 156.0.1 · GeckoView Android 156.0.\n\nAPK de développement signé, débogage via ADB autorisé. Appels/SMS délégués aux applications Android. Aucun engagement de compatibilité avec tous les appareils ; fonctions matérielles et services distants encore incomplets. L’historique ci-dessous distingue les tests de chaque plateforme.\n\n# 🇬🇧 Vulpes OS — Preview 20\n\nExperimental APK and shared desktop / Android / Tundra sources updated since Preview 17. Stages 18 and 19 are included; no additional older APKs are published.\n\n- Declared minimum Android 8 / API 26, target API 37. ARM64, ARMv7 and x86_64. Installing over a Preview signed with the same key preserves its data.\n- Verify your download with `sha256sum -c SHA256SUMS.txt`.\n- Desktop: `./setup.sh`, then `./run.sh`. Sargo sources included; this release supplies no new native image.\n- Vulpes 2.7-preview.20 · Gaia 2.7.0 · desktop/Sargo Gecko 156.0.1 · Android GeckoView 156.0.\n\nSigned development APK, debugging through authorized ADB. Calls/SMS delegated to Android applications. Not every device is qualified; hardware functions and remote services remain incomplete. The history below separates platform-specific test results.\n" } } } diff --git a/gaia/apps/camera/js/controllers/activity.js b/gaia/apps/camera/js/controllers/activity.js index 9be7de3f..3f167e07 100755 --- a/gaia/apps/camera/js/controllers/activity.js +++ b/gaia/apps/camera/js/controllers/activity.js @@ -272,7 +272,7 @@ ActivityController.prototype.onActivityConfirmed = function(newMedia) { // Video if (newMedia.isVideo) { - media.type = 'video/3gpp'; + media.type = media.blob.type || 'video/3gpp'; media.poster = newMedia.poster.blob; activity.postResult(media); this.app.clearSpinner(); diff --git a/gaia/apps/camera/js/controllers/overlay.js b/gaia/apps/camera/js/controllers/overlay.js index 952d8633..89d29fa1 100755 --- a/gaia/apps/camera/js/controllers/overlay.js +++ b/gaia/apps/camera/js/controllers/overlay.js @@ -107,7 +107,7 @@ OverlayController.prototype.updateOverlay = function(type, enabled, reason) { return; } - var closable = self.activity.pick && type !== 'request-fail'; + var closable = self.activity.pick || reason === 'request-fail'; var view = new OverlayView({ type: reason, closable: closable, @@ -118,6 +118,10 @@ OverlayController.prototype.updateOverlay = function(type, enabled, reason) { overlay.view = view.appendTo(document.body) .on('click:close-btn', function() { + if (reason === 'request-fail' && !self.activity.pick) { + self.updateOverlay(type, false); + return; + } self.app.emit('activitycanceled'); }); diff --git a/gaia/apps/camera/js/controllers/storage.js b/gaia/apps/camera/js/controllers/storage.js index 0f196f32..23a8058c 100755 --- a/gaia/apps/camera/js/controllers/storage.js +++ b/gaia/apps/camera/js/controllers/storage.js @@ -147,7 +147,7 @@ StorageController.prototype.storeVideo = function(video) { var self = this; // Add the poster image to the image storage - poster.filepath = video.filepath.replace('.3gp', '.jpg'); + poster.filepath = video.filepath.replace(/\.(?:3gp|mp4|webm)$/, '.jpg'); video.isVideo = true; this.storage.addPicture( diff --git a/gaia/apps/camera/js/controllers/viewfinder.js b/gaia/apps/camera/js/controllers/viewfinder.js index ccd9d398..8462f776 100755 --- a/gaia/apps/camera/js/controllers/viewfinder.js +++ b/gaia/apps/camera/js/controllers/viewfinder.js @@ -102,6 +102,7 @@ ViewfinderController.prototype.hideGrid = function() { * @private */ ViewfinderController.prototype.bindEvents = function() { + window.addEventListener('resize', this.configurePreview); // View this.views.viewfinder.on('fadedin', this.app.firer('viewfinder:visible')); diff --git a/gaia/apps/camera/js/lib/dcf.js b/gaia/apps/camera/js/lib/dcf.js index 98ffb082..ba28ba98 100755 --- a/gaia/apps/camera/js/lib/dcf.js +++ b/gaia/apps/camera/js/lib/dcf.js @@ -25,7 +25,7 @@ var dcfConfig = { seq: null, postFix: 'MZLLA', prefix: {video: 'VID_', image: 'IMG_'}, - ext: {video: '3gp', image: 'jpg'} + ext: {video: window.VulpesVideoExtension || '3gp', image: 'jpg'} }; exports.init = function() { diff --git a/gaia/apps/camera/js/lib/storage.js b/gaia/apps/camera/js/lib/storage.js index fcc18ac4..e92f6a5c 100755 --- a/gaia/apps/camera/js/lib/storage.js +++ b/gaia/apps/camera/js/lib/storage.js @@ -252,7 +252,7 @@ Storage.prototype.checkFilepath = function(filepath) { // image and get the video filepath by changing '.jpg' to '.3gp' if (filepath.indexOf('VID') != -1 && filepath.lastIndexOf('.jpg') === filepath.length - 4) { - filepath = filepath.replace('.jpg', '.3gp'); + filepath = filepath.replace('.jpg', '.' + (window.VulpesVideoExtension || '3gp')); } return filepath; @@ -380,7 +380,7 @@ Storage.prototype.deletePicture = function(filepath, done) { * @param {String} filepath */ Storage.prototype.deleteVideo = function(filepath) { - var poster = filepath.replace('.3gp', '.jpg'); + var poster = filepath.replace(/\.(?:3gp|mp4|webm)$/, '.jpg'); this.video.delete(filepath).onerror = function(e) { console.warn( diff --git a/gaia/apps/camera/js/views/viewfinder.js b/gaia/apps/camera/js/views/viewfinder.js index ed6f508a..d89e3c23 100755 --- a/gaia/apps/camera/js/views/viewfinder.js +++ b/gaia/apps/camera/js/views/viewfinder.js @@ -205,6 +205,9 @@ module.exports = View.extend({ */ updatePreview: function(preview, sensorAngle, mirrored) { if (!preview) { return; } + // An embedded app can initialize before its window is visible. + this.getSize(); + if (!this.width || !this.height) { return; } var aspect; // Invert dimensions if the camera's `sensorAngle` is diff --git a/gaia/apps/communications/dialer/js/telephony_helper.js b/gaia/apps/communications/dialer/js/telephony_helper.js index 6d7b7052..cdf2ce0c 100755 --- a/gaia/apps/communications/dialer/js/telephony_helper.js +++ b/gaia/apps/communications/dialer/js/telephony_helper.js @@ -25,6 +25,19 @@ var TelephonyHelper = (function() { return Promise.reject(); } + // The platform adapter owns network checks on Tundra and Android. + if (window.VulpesPlatformDial) { + return window.VulpesPlatformDial(sanitizedNumber.split(',')[0]).then(function(obj) { + if (window.TelephonyCall && obj instanceof TelephonyCall) { + installHandlers(obj, sanitizedNumber, false, cardIndex, + oncall, onconnected, ondisconnected); + } else if (oncall) { + oncall(); + } + return obj; + }); + } + var conn = navigator.mozMobileConnections && navigator.mozMobileConnections[cardIndex]; @@ -170,11 +183,13 @@ var TelephonyHelper = (function() { call.addEventListener('connected', onconnected); call.addEventListener('disconnected', function handleDisconnect(evt) { - loadTelephonyMessages(function() { - TelephonyMessages.handleDisconnect( - evt.call.disconnectedReason, number, emergencyOnly - ); - }); + if (evt.call.disconnectedReason) { + loadTelephonyMessages(function() { + TelephonyMessages.handleDisconnect( + evt.call.disconnectedReason, number, emergencyOnly + ); + }); + } ondisconnected && ondisconnected(); }); } diff --git a/gaia/apps/gallery/js/MetadataParser.js b/gaia/apps/gallery/js/MetadataParser.js index 43607764..87fba101 100755 --- a/gaia/apps/gallery/js/MetadataParser.js +++ b/gaia/apps/gallery/js/MetadataParser.js @@ -651,25 +651,30 @@ var metadataParser = (function() { function videoMetadataParser(file, metadataCallback, errorCallback) { var metadata = {}; - var videofilename = file.name.replace('.jpg', '.3gp'); - metadata.video = videofilename; - - var getreq = videostorage.get(videofilename); - getreq.onerror = function() { - errorCallback('cannot get video file: ' + videofilename); - }; - getreq.onsuccess = function() { - var videofile = getreq.result; - getVideoRotation(videofile, function(rotation) { - if (typeof rotation === 'number') { - metadata.rotation = rotation; + var extensions = ['webm', '3gp', 'mp4']; + function findVideo() { + var videofilename = file.name.replace(/\.jpg$/, '.' + extensions.shift()); + var getreq = videostorage.get(videofilename); + getreq.onerror = function() { + if (extensions.length) findVideo(); + else errorCallback('cannot get video file: ' + videofilename); + }; + getreq.onsuccess = function() { + metadata.video = videofilename; + if (/\.webm$/.test(videofilename)) { + metadata.rotation = 0; getVideoThumbnailAndSize(); + return; } - else if (typeof rotation === 'string') { - errorCallback('Video rotation:', rotation); - } - }); - }; + getVideoRotation(getreq.result, function(rotation) { + if (typeof rotation === 'number') { + metadata.rotation = rotation; + getVideoThumbnailAndSize(); + } else errorCallback('Video rotation: ' + rotation); + }); + }; + } + findVideo(); function getVideoThumbnailAndSize() { var url = URL.createObjectURL(file); diff --git a/gaia/apps/settings/js/modules/date_time.js b/gaia/apps/settings/js/modules/date_time.js index 42eae938..300c053c 100755 --- a/gaia/apps/settings/js/modules/date_time.js +++ b/gaia/apps/settings/js/modules/date_time.js @@ -139,10 +139,6 @@ define(function(require) { */ DateTime.prototype._init = function() { this._mozTime = window.navigator.mozTime; - if (!this._mozTime) { - console.error('Could not get window.navigator.mozTime'); - return; - } this._boundSetTimeAutoEnabled = function(event) { this._clockAutoEnabled = event.settingValue; @@ -320,9 +316,7 @@ define(function(require) { switch (type) { case 'date': tokens = value.split('-'); - newDate.setYear(tokens[0]); - newDate.setMonth(tokens[1] - 1); - newDate.setDate(tokens[2]); + newDate.setFullYear(Number(tokens[0]), Number(tokens[1]) - 1, Number(tokens[2])); break; case 'time': @@ -331,7 +325,8 @@ define(function(require) { newDate.setMinutes(parseInt(tokens[1])); break; } - this._mozTime.set(newDate); + if (!this._mozTime) return; + return this._mozTime.set(newDate); }; /** diff --git a/gaia/apps/settings/js/modules/dialog_manager.js b/gaia/apps/settings/js/modules/dialog_manager.js index bc59abdc..1692d23d 100755 --- a/gaia/apps/settings/js/modules/dialog_manager.js +++ b/gaia/apps/settings/js/modules/dialog_manager.js @@ -99,37 +99,35 @@ define(function(require) { _transit: function dm__transit(method, dialog, options) { var promise = new Promise(function(resolve) { var panel = dialog.panel; - - panel.addEventListener('transitionend', function paintWait(evt) { - if ((method === 'close' || method === 'open') && - evt.propertyName === 'visibility') { - // After transition, we have to `hide` the panel, otherwise - // the panel would still exist on the layer and would block - // the scrolling event. - if (method === 'close') { - panel.hidden = true; - } - panel.removeEventListener('transitionend', paintWait); - resolve(); - } - }); - - // Before transition, we have to `show` the panel, otherwise - // the panel before applying transition class. - if (method === 'open') { - panel.hidden = false; + var completed = false; + var fallback; + function finish(event) { + if (event && (event.target !== panel || event.propertyName !== 'visibility')) return; + if (completed) return; + completed = true; + clearTimeout(fallback); + panel.removeEventListener('transitionend', finish); + panel.removeEventListener('transitioncancel', finish); + if (method === 'close') panel.hidden = true; + resolve(); } - - // We need to apply class later otherwise Gecko can't apply - // this transition and 150ms is an approximate number after doing - // several rounds of manual tests. + if (method === 'open') panel.hidden = false; setTimeout(function() { - if (method === 'open') { - // Let's unhide the panel first - panel.classList.add('current'); - } else { - panel.classList.remove('current'); + panel.addEventListener('transitionend', finish); + panel.addEventListener('transitioncancel', finish); + panel.classList.toggle('current', method === 'open'); + var style = window.getComputedStyle(panel); + function milliseconds(value) { + return parseFloat(value) * (value.trim().endsWith('ms') ? 1 : 1000) || 0; } + var durations = style.transitionDuration.split(',').map(milliseconds); + var delays = style.transitionDelay.split(',').map(milliseconds); + var duration = Math.max.apply(Math, durations.map(function(value, index) { + return value + delays[index % delays.length]; + })); + // Reduced motion, display changes or interrupted transitions may + // omit the visibility event. Always release the dialog queue. + fallback = setTimeout(finish, Math.max(0, duration) + 100); }, 150); }); return promise; diff --git a/gaia/apps/settings/js/modules/dialog_service.js b/gaia/apps/settings/js/modules/dialog_service.js index a9188715..8f96465c 100755 --- a/gaia/apps/settings/js/modules/dialog_service.js +++ b/gaia/apps/settings/js/modules/dialog_service.js @@ -190,29 +190,30 @@ define(function(require) { if ('#' + panelId === currentPanel) { defer.reject('You are showing the same panel #' + panelId); } else { - options.onWrapSubmit = function() { - DialogManager.close(dialog, 'submit', options) - .then(function(result) { - defer.resolve({ - type: 'submit', - value: result - }); + var opening; + var closing = false; + function close(type) { + if (closing) return; + closing = true; + // A tap during the opening animation must not run hide/show + // concurrently, or a queued dialog can inherit stale form values. + Promise.resolve(opening).then(function() { + return DialogManager.close(dialog, type, options); + }).then(function(result) { self._navigating = false; + defer.resolve({type: type, value: result}); self._execPendingRequest(); + }, function(error) { + // Panel validation rejects to keep the form open. Allow a + // correction or cancellation without rejecting the show promise. + closing = false; + dialogDOM.dispatchEvent(new CustomEvent('dialogvalidationfailed', { + detail: error + })); }); - }; - - options.onWrapCancel = function() { - DialogManager.close(dialog, 'cancel', options) - .then(function(result) { - defer.resolve({ - type: 'cancel', - value: result - }); - self._navigating = false; - self._execPendingRequest(); - }); - }; + } + options.onWrapSubmit = function() { close('submit'); }; + options.onWrapCancel = function() { close('cancel'); }; switch (options.type) { case 'alert': @@ -229,7 +230,17 @@ define(function(require) { break; } this._navigating = true; - DialogManager.open(dialog, options); + opening = DialogManager.open(dialog, options); + opening.catch(function(error) { + // A missing host API must not lock every later Settings dialog. + dialogDOM.classList.remove('current'); + dialogDOM.hidden = true; + var overlay = document.querySelector('.settings-dialog-overlay'); + if (overlay) overlay.hidden = true; + self._navigating = false; + defer.reject(error); + self._execPendingRequest(); + }); } } diff --git a/gaia/apps/settings/js/modules/settings_service.js b/gaia/apps/settings/js/modules/settings_service.js index 4b81f1cf..767adbef 100755 --- a/gaia/apps/settings/js/modules/settings_service.js +++ b/gaia/apps/settings/js/modules/settings_service.js @@ -246,6 +246,17 @@ define(function(require) { // directly Settings._currentPanel = '#' + panelId; callback(); + }).catch(function(error) { + console.error('Settings panel ' + panelId + ':', error); + newPanelElement.classList.remove('current'); + if (currentPanelElement) currentPanelElement.classList.add('current'); + // Release the navigation lock even when a hardware query fails. + callback(error); + if (window.VulpesGaiaUI) { + window.VulpesGaiaUI.notice(document.documentElement.lang.indexOf('fr') === 0 ? + 'Ce panneau n’a pas pu être chargé. Vous pouvez revenir aux autres paramètres.' : + 'This panel could not be loaded. You can return to the other settings.'); + } }); }); }); diff --git a/gaia/apps/settings/js/panels/app_permissions_list/app_permissions_list.js b/gaia/apps/settings/js/panels/app_permissions_list/app_permissions_list.js index e5f3b161..7e01cc98 100755 --- a/gaia/apps/settings/js/panels/app_permissions_list/app_permissions_list.js +++ b/gaia/apps/settings/js/panels/app_permissions_list/app_permissions_list.js @@ -103,9 +103,10 @@ define(function(require) { * @access public */ _onAppChoose: function pl__onAppChoose(evt) { - if (evt.target.dataset && evt.target.dataset.appIndex) { + var target = evt.target.closest('[data-app-index]'); + if (target && this._listRoot.contains(target)) { SettingsService.navigate('appPermissions-details', { - app: this._apps[evt.target.dataset.appIndex], + app: this._apps[target.dataset.appIndex], permissionsTable: this._permissionTable }); } @@ -262,7 +263,7 @@ define(function(require) { * @return {Bool} */ _isExplicitPerm: function pl_isExplicitPerm(perm) { - return mozPerms.isExplicit(perm, this._currentApp.manifestURL, + return mozPerms && mozPerms.isExplicit(perm, this._currentApp.manifestURL, this._currentApp.origin, false); }, diff --git a/gaia/apps/settings/js/panels/help/support.js b/gaia/apps/settings/js/panels/help/support.js index 24a2e565..01c77d96 100755 --- a/gaia/apps/settings/js/panels/help/support.js +++ b/gaia/apps/settings/js/panels/help/support.js @@ -21,7 +21,7 @@ define(function(require) { * refer to the JSON File Data. */ this._callSupportInfo = null; - var url = 'http://support.mozilla.org/products/firefox-os'; + var url = 'https://vulpes-os.org/'; this._elements.userGuide.addEventListener('click', () => { this._elements.userGuide.blur(); var activity = new window.MozActivity({ @@ -55,7 +55,10 @@ define(function(require) { .then(function loadSupportInfo(data) { this._supportInfo = data; callback(this._supportInfo); - }.bind(this)); + }.bind(this)).catch(function(error) { + console.warn('Support information:', error); + callback({onlinesupport: {title: 'Vulpes OS', href: 'https://vulpes-os.org/'}, callsupport: []}); + }); }, /** * create support url items @@ -103,7 +106,7 @@ define(function(require) { _renderSupportInfo: function support_renderSupportInfo(result) { var onlineSupportInfo = null; var onlineSupportTitle = result['support.onlinesupport.title']; - if (onlineSupportTitle !== '') { + if (typeof onlineSupportTitle === 'string' && onlineSupportTitle !== '') { onlineSupportInfo = { title: onlineSupportTitle }; onlineSupportInfo.href = result['support.onlinesupport.href']; this._setOnlineSupportInfo(onlineSupportInfo); @@ -114,7 +117,7 @@ define(function(require) { // Check to see if we have a title for the first support number. var callSupport1Title = result['support.callsupport1.title']; // If we have a title we'll go ahead and load the href for it too. - if (callSupport1Title !== '') { + if (typeof callSupport1Title === 'string' && callSupport1Title !== '') { this._callSupportInfo = [ { 'title': callSupport1Title, @@ -125,7 +128,7 @@ define(function(require) { // Now check to see if we have a title for the second // support number. If we do, we'll load it's href as well. var callSupport2Title = result['support.callsupport2.title']; - if (callSupport2Title !== '') { + if (typeof callSupport2Title === 'string' && callSupport2Title !== '') { this._callSupportInfo.push({ 'title': callSupport2Title, 'href': result['support.callsupport2.href'] diff --git a/gaia/apps/settings/js/panels/screen_lock_passcode/panel.js b/gaia/apps/settings/js/panels/screen_lock_passcode/panel.js index b13186d2..d87a0810 100755 --- a/gaia/apps/settings/js/panels/screen_lock_passcode/panel.js +++ b/gaia/apps/settings/js/panels/screen_lock_passcode/panel.js @@ -66,8 +66,11 @@ define(function(require) { }, _bindEvents: function() { - this._elements.passcodeInput.onkeypress = - this._onPasscodeInputKeypress.bind(this); + this._elements.passcodeInput.type = 'password'; + this._elements.passcodeInput.inputMode = 'numeric'; + this._elements.passcodeInput.maxLength = 8; + this._elements.passcodeInput.oninput = + this._onPasscodeInput.bind(this); this._elements.passcodeContainer.onclick = this._onPasscodeContainerClick.bind(this); }, @@ -77,37 +80,14 @@ define(function(require) { this._elements.passcodeInput.focus(); }, - _onPasscodeInputKeypress: function(evt) { - // key code for key strokes from the keypad are 0 (numbers) and 8 - // (backspace). Filter out the events that are not from the keypad. - var keyCode = evt.keyCode; - if (keyCode !== 0 && keyCode !== 8) { - return; + _onPasscodeInput: function() { + // Android IMEs commit text without legacy keypress/charCode events. + var value = this._elements.passcodeInput.value.replace(/[^0-9]/g, '').slice(0, 8); + this._hideErrorMessage(); + if (this._elements.panel.dataset.passcodeStatus === 'success') { + this._resetPasscodeStatus(); } - - evt.preventDefault(); - - if (this._passcodeBuffer === '') { - this._hideErrorMessage(); - } - - var code = evt.charCode; - if (code !== 0 && (code < 0x30 || code > 0x39)) { - return; - } - - var key = String.fromCharCode(code); - if (evt.charCode === 0) { - if (this._passcodeBuffer.length > 0) { - this._passcodeBuffer = this._getPasscodeBuffer('all'); - if (this._elements.panel.dataset.passcodeStatus === 'success') { - this._resetPasscodeStatus(); - } - } - } else if (this._passcodeBuffer.length < 8) { - this._passcodeBuffer += key; - } - + this._passcodeBuffer = value; this._updatePassCodeUI(); this._processPasscode(); }, @@ -134,6 +114,7 @@ define(function(require) { }, _updatePassCodeUI: function() { + this._elements.passcodeInput.value = this._passcodeBuffer; for (var i = 0; i < 8; i++) { if (i < this._passcodeBuffer.length) { this._elements.passcodeDigits[i].dataset.dot = true; diff --git a/gaia/apps/settings/js/panels/simcard_manager/simcard_manager.js b/gaia/apps/settings/js/panels/simcard_manager/simcard_manager.js index 3958d8b4..261320af 100755 --- a/gaia/apps/settings/js/panels/simcard_manager/simcard_manager.js +++ b/gaia/apps/settings/js/panels/simcard_manager/simcard_manager.js @@ -304,7 +304,7 @@ define(function(require) { */ _updateSimSettingsUI: function scm__updateSimSettingsUI() { var firstCardInfo = this._simcards[0].getInfo(); - var secondCardInfo = this._simcards[1].getInfo(); + var secondCardInfo = (this._simcards[1] ? this._simcards[1].getInfo() : {absent: true}); var hidden = firstCardInfo.absent && secondCardInfo.absent || this._isAirplaneMode; @@ -322,7 +322,7 @@ define(function(require) { */ _updateSelectOptionsUI: function scm__updateSelectOptionsUI() { var firstCardInfo = this._simcards[0].getInfo(); - var secondCardInfo = this._simcards[1].getInfo(); + var secondCardInfo = (this._simcards[1] ? this._simcards[1].getInfo() : {absent: true}); // two cards all are not absent, we have to update separately if (!firstCardInfo.absent && !secondCardInfo.absent) { diff --git a/gaia/apps/settings/js/panels/wifi_status/panel.js b/gaia/apps/settings/js/panels/wifi_status/panel.js index e992f3f6..aa325547 100755 --- a/gaia/apps/settings/js/panels/wifi_status/panel.js +++ b/gaia/apps/settings/js/panels/wifi_status/panel.js @@ -41,7 +41,8 @@ define(function(require) { var info = wifiManager.connectionInformation || {}; elements.ip.textContent = info.ipAddress || ''; document.l10n.setAttributes(elements.speed, - 'linkSpeedMbs', { linkSpeed: info.linkSpeed }); + Number.isFinite(info.linkSpeed) ? 'linkSpeedMbs' : 'unavailable', + Number.isFinite(info.linkSpeed) ? { linkSpeed: info.linkSpeed } : {}); } }); }; diff --git a/gaia/apps/verticalhome/js/stores/item.js b/gaia/apps/verticalhome/js/stores/item.js index e5fd249c..5da09703 100755 --- a/gaia/apps/verticalhome/js/stores/item.js +++ b/gaia/apps/verticalhome/js/stores/item.js @@ -439,7 +439,8 @@ */ populate: function(callback) { this.initSources(function(entries) { - this.deferredSave(entries, callback); + // The first render waits for this write; deferring it waits for that render. + this.save(entries, callback); }.bind(this)); }, diff --git a/gaia/apps/video/js/video.js b/gaia/apps/video/js/video.js index ebeaf7a7..ab1cce7c 100755 --- a/gaia/apps/video/js/video.js +++ b/gaia/apps/video/js/video.js @@ -102,7 +102,7 @@ var currentLayoutMode; var pendingUpdateTitleText = false; // Videos recorded by our own camera have filenames of this form -var FROMCAMERA = /DCIM\/\d{3}MZLLA\/VID_\d{4}\.3gp$/; +var FROMCAMERA = /DCIM\/\d{3}MZLLA\/VID_\d{4}\.(?:3gp|mp4|webm)$/; // We have a single instance of the loading checker because it is used // across functions @@ -664,7 +664,7 @@ function deleteFile(filename) { // If we're deleting a video file recorded by our camera, // we also need to delete the poster image associated with // that video. - var postername = filename.replace('.3gp', '.jpg'); + var postername = filename.replace(/\.(?:3gp|mp4|webm)$/, '.jpg'); navigator.getDeviceStorage('pictures'). delete(postername); } diff --git a/gaia/apps/wallpaper/style/wallpaper.css b/gaia/apps/wallpaper/style/wallpaper.css index 0a581104..51e131ed 100755 --- a/gaia/apps/wallpaper/style/wallpaper.css +++ b/gaia/apps/wallpaper/style/wallpaper.css @@ -70,3 +70,6 @@ html[dir="rtl"] .wallpaper { margin: 0 0.5rem; } + +/* Keep preview tiles aligned when source wallpapers have different ratios. */ +#wallpapers .wallpaper { aspect-ratio: 2 / 3; object-fit: cover; display: block; } diff --git a/gaia/shared/js/download/download_helper.js b/gaia/shared/js/download/download_helper.js index 70ff7080..58427d5e 100755 --- a/gaia/shared/js/download/download_helper.js +++ b/gaia/shared/js/download/download_helper.js @@ -113,6 +113,10 @@ var DownloadHelper = (function() { */ function getBlob(download) { var req = new Request(); + if (download.vulpesDownload) { + download.getBlob().then(function(blob) { req.done(blob); }, function(error) { req.failed(error); }); + return req; + } var storage = getVolume(download.storageName); var storeAvailableReq = storage.available(); @@ -394,6 +398,10 @@ var DownloadHelper = (function() { */ function remove(download) { var req = new Request(); + if (download.vulpesDownload) { + navigator.mozDownloadManager.remove(download).then(function() { req.done(download); }, function(error) { req.failed(error); }); + return req; + } var incompleteDownload = download.state !== 'succeeded'; // If is not done, use download manager to remove it, // otherwise, deal with the datastore. diff --git a/host/Downloads.sys.mjs b/host/Downloads.sys.mjs new file mode 100644 index 00000000..8e62a4c0 --- /dev/null +++ b/host/Downloads.sys.mjs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MPL-2.0 +// Download identities belong to Gecko; paths supplied by content are never used. +export class GaiaDownloads { + constructor() {this.ids=new WeakMap();this.items=new Map();} + async request(app, params={}) { + if(!['settings','system'].includes(app.id))throw Error('PERMISSION_DENIED'); + if(!['list','pause','resume','remove','blob'].includes(params.operation))throw Error('INVALID_VALUE'); + const {Downloads}=ChromeUtils.importESModule('resource://gre/modules/Downloads.sys.mjs'); + const list=await Downloads.getList(Downloads.PUBLIC); + const entries=await list.getAll(); + this.items.clear(); + const rows=entries.map(item=>{ + if(!this.ids.has(item))this.ids.set(item,Services.uuid.generateUUID().toString()); + const id=this.ids.get(item);this.items.set(id,item); + return {id,url:item.source.url,path:item.target.path,startTime:+item.startTime, + contentType:item.contentType||'application/octet-stream',currentBytes:item.currentBytes,totalBytes:item.totalBytes, + state:item.succeeded?'succeeded':item.stopped?'stopped':'downloading'}; + }); + if(params.operation==='list')return rows; + const item=this.items.get(params.id);if(!item)throw Error('DOWNLOAD_NOT_FOUND'); + if(params.operation==='blob') { + if(!item.succeeded)throw Error('DOWNLOAD_NOT_COMPLETE'); + return File.createFromFileName(item.target.path); + } + if(params.operation==='pause')await item.cancel(); + if(params.operation==='resume')await item.start(); + if(params.operation==='remove') {await item.cancel();await item.finalize(true);if(item.succeeded)await IOUtils.remove(item.target.path,{ignoreAbsent:true});await list.remove(item);} + return true; + } +} diff --git a/host/Input.sys.mjs b/host/Input.sys.mjs index 6c73c982..d4b8fc75 100644 --- a/host/Input.sys.mjs +++ b/host/Input.sys.mjs @@ -1,36 +1 @@ -// Only the bundled keyboard can edit the field reported by a content actor. -export class Input { - constructor(runtime) { this.runtime=runtime; this.focus=null; this.sequence=0; } - changed(actor, state) { - const app=this.runtime.identity(actor); - if (!app || app.id==='keyboard') return; - if (!state) { if(this.focus?.actor===actor) this.hide(); return; } - this.focus={actor,state:{...state,id:++this.sequence}}; - this.runtime.broadcast('input-focus',this.focus.state,'system'); - this.runtime.broadcast('input-focus',this.focus.state,'keyboard'); - } - hide() { - this.focus=null; - this.runtime.broadcast('input-focus',null,'system'); - this.runtime.broadcast('input-focus',null,'keyboard'); - } - cleanup(actor) { if(this.focus?.actor===actor) this.hide(); } - async request(actor, p) { - const app=this.runtime.identity(actor); - if (!app || !['keyboard','system'].includes(app.id)) throw Error('PERMISSION_DENIED'); - if (p.operation==='hide') { this.hide();return null; } - if(app.id!=='keyboard') throw Error('PERMISSION_DENIED'); - if(p.operation==='state') return this.focus?.state || null; - if(p.operation==='height') { - if(!Number.isFinite(p.height) || p.height<0 || p.height>600) throw Error('INVALID_VALUE'); - this.runtime.broadcast('input-height',{height:p.height},'system');return null; - } - if(!this.focus || p.id!==this.focus.state.id) throw Error('INPUT_CONTEXT_EXPIRED'); - const context=this.focus; - const result=await context.actor.sendQuery('Vulpes:Edit',p); - // An application can close or focus another field while the edit is pending. - if(this.focus!==context) throw Error('INPUT_CONTEXT_EXPIRED'); - Object.assign(context.state,result); - return context.state; - } -} +export {Input} from 'resource://vulpes/services/input.mjs'; diff --git a/host/Messages.sys.mjs b/host/Messages.sys.mjs index 02a88f0b..6f6730e2 100644 --- a/host/Messages.sys.mjs +++ b/host/Messages.sys.mjs @@ -1,10 +1,11 @@ // Desktop message store. Radio operations fail explicitly; no synthetic SMS. export class Messages { - constructor(storage, radio = null, notify = () => {}, radioStatus = null) { + constructor(storage, radio = null, notify = () => {}, radioStatus = null, mms = null) { this.storage = storage; this.radio = radio; this.notify = notify; this.radioStatus = radioStatus; + this.mms = mms; this.queue = Promise.resolve(); } receive(incoming) { @@ -14,7 +15,8 @@ export class Messages { if (existing) return existing; const id=Math.max(Date.now(),...records.map(m=>Number(m.id)+1 || 0)); const record={id,threadId:records.find(m=>(m.sender||m.receiver)===incoming.sender)?.threadId || id+1, - receipt:incoming.receipt,type:'sms',sender:incoming.sender,receiver:null, + receipt:incoming.receipt,type:incoming.type || 'sms',sender:incoming.sender,receiver:null, + ...(incoming.type==='mms' ? {receivers:incoming.receivers || [],subject:incoming.subject || '',smil:incoming.smil || '',attachments:incoming.attachments || [],deliveryInfo:[]} : {}), body:incoming.body,timestamp:incoming.timestamp,delivery:'received',read:false}; records.push(record); await this.storage.run('vulpes-private-messages','put',[records,'records']); @@ -24,14 +26,16 @@ export class Messages { return work; } refreshOutgoing() { - if (!this.radioStatus) return Promise.resolve(); + if (!this.radioStatus && !this.mms) return Promise.resolve(); const work=this.queue.then(async()=>{ const records=(await this.storage.run('vulpes-private-messages','get',['records'])).value || []; for (const message of records.filter(m=>m.delivery==='sending' && m.radioPath)) { - const result=await this.radioStatus(message.radioPath); + const result=await (message.type==='mms' ? this.mms.status(message.radioPath) : this.radioStatus(message.radioPath)); if (!['sent','failed'].includes(result.state)) continue; message.delivery=result.state==='sent'?'sent':'error'; message.deliveryStatus=result.state==='sent'?'not-applicable':'error'; + if(result.error) message.error=result.error; + if(message.type==='mms') message.deliveryInfo=message.receivers.map(receiver=>({receiver,deliveryStatus:message.deliveryStatus,readStatus:'not-applicable'})); await this.storage.run('vulpes-private-messages','put',[records,'records']); this.notify(result.state,structuredClone(message)); } @@ -104,7 +108,34 @@ export class Messages { this.notify('sent', structuredClone(record)); return record; } - if (['send', 'sendMMS', 'retrieveMMS', 'getSmscAddress', 'setSmscAddress'].includes(operation)) + if (operation === 'sendMMS' && this.mms) { + const payload=args[0]; + this.mms.validate(payload); + const receivers=payload.receivers.slice(); + const participants=receivers.slice().sort().join(','); + const existing=records.find(m=>(m.type==='mms' && m.delivery!=='received' ? m.receivers.slice().sort().join(',') : m.receiver || m.sender)===participants); + const record={id:nextId++,threadId:existing?.threadId || nextId++,type:'mms',sender:null, + receivers,subject:payload.subject || '',smil:payload.smil || '',attachments:payload.attachments || [],body:payload.subject || '', + timestamp:Date.now(),delivery:'sending',deliveryStatus:'pending',read:true, + deliveryInfo:receivers.map(receiver=>({receiver,deliveryStatus:'pending',readStatus:'not-applicable'}))}; + records.push(record); + await this.storage.run('vulpes-private-messages','put',[records,'records']); + this.notify('sending',structuredClone(record)); + try { + const submitted=await this.mms.send(payload); + record.radioPath=submitted.path; + await this.storage.run('vulpes-private-messages','put',[records,'records']); + return record; + } catch(error) { + record.delivery='error';record.deliveryStatus='error';record.error=error.message; + record.deliveryInfo=receivers.map(receiver=>({receiver,deliveryStatus:'error',readStatus:'not-applicable'})); + await this.storage.run('vulpes-private-messages','put',[records,'records']); + this.notify('failed',structuredClone(record));throw error; + } + } + if (['sendMMS', 'retrieveMMS'].includes(operation)) + throw new Error('NotSupportedError'); + if (['send', 'getSmscAddress', 'setSmscAddress'].includes(operation)) throw new Error('RadioDisabledError'); if (operation === 'getThreads') { const threads = new Map(); @@ -115,7 +146,7 @@ export class Messages { id: message.threadId, body: message.body, timestamp: message.timestamp, - participants: [message.sender || message.receiver], + participants: message.delivery==='received' ? [message.sender] : message.type==='mms' ? message.receivers : [message.receiver], unreadCount: records.filter((m) => m.threadId === message.threadId && !m.read).length, lastMessageType: message.type, }); diff --git a/host/NativeCamera.sys.mjs b/host/NativeCamera.sys.mjs index 2ae36500..4afb80f1 100644 --- a/host/NativeCamera.sys.mjs +++ b/host/NativeCamera.sys.mjs @@ -17,8 +17,9 @@ export class NativeCamera { return await Promise.race([ (async () => { const pipe=session.process.stdout; - const sequence=metadata ? null : await pipe.readUint32(); - const length=await pipe.readUint32(); + const header=new DataView(await pipe.read(metadata ? 4 : 8)); + const sequence=metadata ? null : header.getUint32(0,true); + const length=header.getUint32(metadata ? 0 : 4,true); if(metadata) { if(length>65536) throw Error('CAMERA_INVALID_FORMAT'); return pipe.readJSON(length); diff --git a/host/Platform.sys.mjs b/host/Platform.sys.mjs index 84fe5cfb..4b8503cc 100644 --- a/host/Platform.sys.mjs +++ b/host/Platform.sys.mjs @@ -1,9 +1,12 @@ +import {receivedMms} from 'resource://vulpes/services/mms.mjs'; import { authorizePlatform } from 'resource://vulpes/services/platform.mjs'; import { tundraCapabilities, + tundraBluetooth, tundraCallAudio, tundraHardware, tundraRadio, + tundraMms, tundraSnapshot, tundraWifi, } from 'resource://vulpes/host/Tundra.sys.mjs'; @@ -48,6 +51,25 @@ export class Platform { } async init() { this.radioQueue = Promise.resolve(); + if (Services.env.get('VULPES_TUNDRA') === '1') { + let shown = null; + const pollBluetooth = async () => { + try { + const state = await tundraBluetooth({operation:'status'}); + const pending = state.pairing?.[0]; + const app = this.runtime.apps.find(a=>a.id==='bluetooth'); + if (pending && app && pending.id!==shown) { + shown=pending.id; + this.runtime.broadcast('launch',{manifestURL:app.manifestURL,origin:app.origin, + url:app.origin+'/onpair.html',timestamp:Date.now()},'system'); + } + if (!pending) shown=null; + } catch (_) {} + setTimeout(pollBluetooth,1500); + }; + pollBluetooth(); + } + this.notificationPath = PathUtils.join(PathUtils.profileDir,'vulpes-notifications.json'); let notifications = []; try { notifications = await IOUtils.readJSON(this.notificationPath); } catch (_) {} @@ -80,6 +102,16 @@ export class Platform { await tundraRadio('ackMessages',{ids:[row.receipt]}); } } catch(error) {console.error('SMS inbox:',error.message);} + try { + const rows=await tundraMms('incoming'); + const app=this.runtime.apps.find(a=>a.id==='sms'); + for(const row of rows) { + const message=await this.runtime.messages.receive(receivedMms(row)); + await this.gaiaNotification(app,{operation:'show',title:message.sender,body:message.body || 'MMS',tag:'sms-'+message.id,data:{threadId:message.threadId}}); + this.runtime.broadcast('sms-received',message,'sms'); + await tundraMms('ack',{ids:[row.receipt]}); + } + } catch(error) {console.error('MMS inbox:',error.message);} setTimeout(inbox,2000); }; inbox(); @@ -136,48 +168,39 @@ export class Platform { !request.secure || request.sharingScreen || request.sharingAudio || - request.audioInputDevices?.length || - !request.videoInputDevices?.length + !(request.videoInputDevices?.length || request.audioInputDevices?.length) ) { deny(); return; } const principal = global.documentPrincipal; - let permission = Services.perms.testExactPermissionFromPrincipal(principal, 'camera'); - if (permission === Ci.nsIPermissionManager.UNKNOWN_ACTION) { - const allowed = Services.prompt.confirm( - browser.ownerDocument.defaultView, - 'Vulpes — Photo', - 'Autoriser Photo à utiliser la webcam ? / Allow Camera to use your webcam?', - ); - permission = allowed - ? Ci.nsIPermissionManager.ALLOW_ACTION - : Ci.nsIPermissionManager.DENY_ACTION; - Services.perms.addFromPrincipal(principal, 'camera', permission); + const video = request.videoInputDevices?.[0], audio = request.audioInputDevices?.[0]; + for (const [device, kind, message] of [ + [video, 'camera', 'Autoriser Photo à utiliser la caméra ? / Allow Camera to use your camera?'], + [audio, 'microphone', 'Autoriser Photo à enregistrer le son ? / Allow Camera to record audio?'] + ]) { + if (!device) continue; + let permission = Services.perms.testExactPermissionFromPrincipal(principal, kind); + if (permission === Ci.nsIPermissionManager.UNKNOWN_ACTION) { + permission = Services.prompt.confirm(browser.ownerDocument.defaultView, 'Vulpes — Photo', message) + ? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION; + Services.perms.addFromPrincipal(principal, kind, permission); + } + if (permission !== Ci.nsIPermissionManager.ALLOW_ACTION) { deny(); return; } } - if (permission !== Ci.nsIPermissionManager.ALLOW_ACTION) { - deny(); - return; - } - if (!(await actor.checkOSPermission(true, false, false))) { - deny(); - return; - } - // Gecko consumes this one-shot grant when allocating the real device. - // Use its top-level principal, as Firefox's WebRTC permission handler does. + if (!(await actor.checkOSPermission(!!video, !!audio, false))) { deny(); return; } const topPrincipal = browser.browsingContext.currentWindowGlobal.documentPrincipal; - Services.perms.addFromPrincipal( - topPrincipal, - 'MediaManagerVideo', - Ci.nsIPermissionManager.ALLOW_ACTION, - Ci.nsIPermissionManager.EXPIRE_SESSION, - ); - const device = request.videoInputDevices[0]; - actor.activateDevicePerm(request.windowID, device.mediaSource, device.rawId); + const devices = []; + for (const [device, kind] of [[video, 'MediaManagerVideo'], [audio, 'MediaManagerAudio']]) { + if (!device) continue; + Services.perms.addFromPrincipal(topPrincipal, kind, Ci.nsIPermissionManager.ALLOW_ACTION, Ci.nsIPermissionManager.EXPIRE_SESSION); + actor.activateDevicePerm(request.windowID, device.mediaSource, device.rawId); + devices.push(device.deviceIndex); + } actor.sendAsyncMessage('webrtc:Allow', { callID: request.callID, windowID: request.windowID, - devices: [request.videoInputDevices[0].deviceIndex], + devices, }); } async request(actor, app, operation, params) { @@ -193,6 +216,9 @@ export class Platform { if (operation === 'snapshot') return tundraSnapshot(); if (operation === 'power') return tundraHardware({operation:'power', action:params.action}); if (operation === 'wifi') return tundraWifi(params); + if (operation === 'deviceSettings') return tundraRadio('deviceSettings', params); + if (operation === 'mobileSettings') return tundraRadio('mobileSettings', params); + if (operation === 'bluetooth') return tundraBluetooth(params); if (['callHistory','ackCallHistory'].includes(operation)) return this.radio(operation, params); if (operation === 'dial') return this.radio('dial', params); if (operation === 'hangup') return tundraRadio('hangup', params); diff --git a/host/Runtime.sys.mjs b/host/Runtime.sys.mjs index e328a542..381f999c 100644 --- a/host/Runtime.sys.mjs +++ b/host/Runtime.sys.mjs @@ -1,3 +1,4 @@ +import {mmsPayload} from 'resource://vulpes/services/mms.mjs'; import { setInterval, setTimeout, clearTimeout } from 'resource://gre/modules/Timer.sys.mjs'; import {Input} from 'resource://vulpes/host/Input.sys.mjs'; import { ScreenPower } from 'resource://vulpes/services/screen-power.mjs'; @@ -13,7 +14,7 @@ import { Messages } from 'resource://vulpes/host/Messages.sys.mjs'; import { Alarms } from 'resource://vulpes/host/Alarms.sys.mjs'; import { Platform } from 'resource://vulpes/host/Platform.sys.mjs'; import { Media } from 'resource://vulpes/host/Media.sys.mjs'; -import { tundraCallAudio, tundraHardware, tundraRadio } from 'resource://vulpes/host/Tundra.sys.mjs'; +import { tundraCallAudio, tundraHardware, tundraRadio, tundraMms } from 'resource://vulpes/host/Tundra.sys.mjs'; Cu.importGlobalProperties(['indexedDB']); // This is the only privileged owner of application identity and settings data. @@ -111,6 +112,7 @@ export const Runtime = { } Services.prefs.setStringPref('intl.accept_languages', this.values['language.current']); this.activities = new Activities({identity:actor=>this.identity(actor),apps:()=>this.apps, + choose:data=>this.broadcast('activity-choice',data,'system'), uuid:()=>Services.uuid.generateUUID().toString(), notify:(actor,type,data)=>actor.sendAsyncMessage('Vulpes:Event',{type,data}), launch:(target,path,{reload=false,entryPoint}={})=>this.broadcast(reload?'activity-launch':'launch',{manifestURL:target.manifestURL,origin:target.origin, @@ -123,6 +125,19 @@ export const Runtime = { const values = { ...this.values, ...Object.fromEntries(entries) }; await IOUtils.writeJSON(path, values, { tmpPath: path + '.tmp' }); this.values = values; + const prefMap={ + 'ui.click_hold_context_menus.delay':'ui.click_hold_context_menus.delay', + 'accessibility.monoaudio.enable':'accessibility.monoaudio.enable', + 'geolocation.enabled':'geo.enabled', + 'privacy.donottrackheader.enabled':'privacy.donottrackheader.enabled', + 'debug.paint-flashing.enabled':'nglayout.debug.paint_flashing', + 'apz.overscroll.enabled':'apz.overscroll.enabled' + }; + for(const [key,value] of entries) { + const pref=prefMap[key];if(!pref)continue; + if(typeof value==='boolean')Services.prefs.setBoolPref(pref,value); + else if(key==='ui.click_hold_context_menus.delay' && [400,800,1600].includes(value))Services.prefs.setIntPref(pref,value); + } if (entries.some(([key]) => key === 'language.current')) Services.prefs.setStringPref('intl.accept_languages', values['language.current']); if (Services.env.get('VULPES_TUNDRA') === '1') { @@ -156,6 +171,8 @@ export const Runtime = { : null, (type, message) => this.broadcast('sms-' + type, message, 'sms'), path => tundraRadio('messageState', {path}), + Services.env.get('VULPES_TUNDRA_RADIO') === '1' ? {validate:mmsPayload, + send:message=>tundraMms('send',mmsPayload(message)), status:path=>tundraMms('state',{path})} : null, ); this.media = new Media((data) => this.broadcast('mediachange', data)); this.platform = new Platform(this); @@ -301,9 +318,10 @@ export const Runtime = { } } if (typeof method !== 'string') return { error: { code: 'INVALID_REQUEST' } }; - if (['activities.subscribe','activities.finish','activities.cancel'].includes(method) || (method === 'activities.start' && isGaiaActivity(params))) { + if (['activities.subscribe','activities.finish','activities.cancel','activities.select'].includes(method) || (method === 'activities.start' && isGaiaActivity(params))) { try { - const result = method === 'activities.cancel' ? this.activities.cancel(actor) + const result = method === 'activities.select' ? this.activities.select(actor,params) + : method === 'activities.cancel' ? this.activities.cancel(actor) : method === 'activities.subscribe' ? this.activities.subscribe(actor) : method === 'activities.finish' ? this.activities.finish(actor,params) : await this.activities.start(actor,params); @@ -359,11 +377,28 @@ export const Runtime = { return { error: { code: error.message || 'VIEW_ERROR' } }; } } + if (method === 'storage.stats') { + try {const {storageStats}=ChromeUtils.importESModule('resource://vulpes/host/StorageStats.sys.mjs');return {result:await storageStats(app)};} + catch(error) {return {error:{code:error.message || 'STORAGE_ERROR'}};} + } + if (method === 'downloads.request') { + try { + const {GaiaDownloads}=ChromeUtils.importESModule('resource://vulpes/host/Downloads.sys.mjs'); + this.downloads ||= new GaiaDownloads(); + return {result:await this.downloads.request(app,params)}; + } catch(error) {return {error:{code:error.message || 'DOWNLOAD_ERROR'}};} + } + if (method === 'appearance.current') return { result: this.values['vulpes.theme'] || 'light' }; if (method === 'locale.current') return { result: this.values['language.current'] || 'en-US' }; if (method === 'settings.snapshot') return this.grants(app).includes('settings.read') ? { result: { ...this.values } } : { error: { code: 'PERMISSION_DENIED' } }; + if (method === 'settings.clockChanged') { + if(app.id !== 'settings' || Services.env.get('VULPES_TUNDRA') !== '1') return {error:{code:'PERMISSION_DENIED'}}; + Services.obs.notifyObservers(null, 'system-timezone-changed'); + return {result:true}; + } if (method.startsWith('settings.')) return this.settings.bindSession(app.id, this.grants(app))(request); if (method === 'desktop.idle') { @@ -517,7 +552,7 @@ export const Runtime = { try { const app = this.identity(actor); if (!app || (target && app.id !== target)) continue; - if (type === 'settingchange' && data.key !== 'language.current' && !this.grants(app).includes('settings.read')) continue; + if (type === 'settingchange' && !['language.current', 'vulpes.theme'].includes(data.key) && !this.grants(app).includes('settings.read')) continue; if ( type === 'mediachange' && !app.manifest.permissions?.['device-storage:' + data.type]?.access?.includes('read') diff --git a/host/SettingsAudit.sys.mjs b/host/SettingsAudit.sys.mjs new file mode 100644 index 00000000..952b0144 --- /dev/null +++ b/host/SettingsAudit.sys.mjs @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: MPL-2.0 +// Settings navigation and bounded readback tests, enabled by a private marker. +export async function auditSettings({evaluate,wait,sleep,save}) { + const context=new Set(['wifi-status','wifi-auth','wifi-joinHidden','wifi-wps','wifi-enterCertificateNickname','call-cf-not-reachable-settings','call-cf-no-reply-settings','call-cf-mobile-busy-settings','call-cf-unconditional-settings','call-voiceMailSettings','hotspot-wifiSettings','screenLock-passcode','apn-editor','apn-list','appPermissions-details','addon-details','homescreen-details','frame','simpin-dialog','call-fdnList-add','call-barring-passcode-change','settings-alert-dialog','settings-confirm-dialog','settings-prompt-dialog','ums-partial-warning','template-messaging','full-developer-mode-warning','full-developer-mode-final-warning']); + await evaluate('system','ScreenManager.turnScreenOn(true);if(Service.query("locked"))await Service.request("unlock",{forcibly:true});await VulpesCompat.call("apps.launch",{manifestURL:"http://settings.localhost:8765/manifest.webapp"});return true;'); + await wait('settings','return document.readyState==="complete" && typeof require==="function";'); + const ids=await evaluate('settings','return Array.from(document.querySelectorAll("section[id]"),e=>e.id);'); + const report={scope:'navigation only; no destructive actions, SIM PIN attempts or subscriptions',panels:{}}; + for(const id of ids) { + if(context.has(id)){report.panels[id]={contextRequired:true};continue;} + try { + await evaluate('settings',`window.__vulpesDiagnostics=[];require(['modules/settings_service'],s=>s.navigate(${JSON.stringify(id)},${JSON.stringify(id==='messaging-details'?{cardIndex:0}:{})}));return true;`); + await wait('settings',`return document.querySelector('section.current')?.id===${JSON.stringify(id)};`,25000); + // Slow carrier queries continue after the panel appears. Wait for those + // controls before leaving, rather than flooding the radio worker while + // sweeping every panel in a few milliseconds. + if(id==='call')await wait('settings',"return !document.querySelector('#ril-callerId').disabled;",30000); + if(id==='call-forwarding')await wait('settings',"return !!document.querySelector('#call-forwarding .callForwardingUnconditional[aria-disabled=false]');",30000); + await sleep(450); + report.panels[id]=await evaluate('settings',`const p=document.getElementById(${JSON.stringify(id)});return {opened:true,title:p.querySelector('h1')?.textContent,unavailable:!!p.querySelector('[data-vulpes-unavailable]'),links:Array.from(p.querySelectorAll('a[href^="#"]'),e=>e.getAttribute('href')),controls:Array.from(p.querySelectorAll('input,select,gaia-switch,gaia-checkbox'),e=>({name:e.name||e.id||e.getAttribute('name'),disabled:!!e.disabled,hidden:!e.checkVisibility()})),errors:window.__vulpesDiagnostics};`); + } catch(error) { + report.panels[id]={opened:false,error:String(error),errors:await evaluate('settings','return window.__vulpesDiagnostics;')}; + await evaluate('settings','location.reload();return true;');await sleep(1500); + await wait('settings','return document.readyState==="complete" && typeof require==="function";'); + } + await save(report); + } + report.dialogs={}; + for(const [id,method,options] of [ + ['settings-confirm-dialog','confirm',{}],['settings-prompt-dialog','prompt',{}],['settings-alert-dialog','alert',{}], + ['wifi-auth','show',{network:{ssid:'Audit — not connected',security:['WPA-PSK']},security:'WPA-PSK',sl:3}], + ['wifi-status','show',{network:{ssid:'Audit — not connected',security:[]},security:'',sl:3}], + ['wifi-joinHidden','show',{}],['screenLock-passcode','show',{mode:'create'}] + ]) { + try { + await evaluate('settings',`window.__dialogResult=null;window.__vulpesDiagnostics=[];require(['modules/dialog_service'],d=>d.${method}(${JSON.stringify(method==='show'?id:'cancel')},${JSON.stringify(options)}).then(r=>window.__dialogResult=r,e=>window.__dialogResult={error:String(e)}));return true;`); + await wait('settings',`return document.getElementById(${JSON.stringify(id)}).classList.contains('current');`,3000); + await evaluate('settings',`const p=document.getElementById(${JSON.stringify(id)});const button=p.querySelector('button[type=reset]')||p.querySelector('button[type=submit]');if(button)button.click();else p.querySelector('gaia-header')?.dispatchEvent(new CustomEvent('action'));return true;`); + await wait('settings',`return !!window.__dialogResult;`,3000); + report.dialogs[id]=await evaluate('settings',`return {result:window.__dialogResult,errors:window.__vulpesDiagnostics,closed:!document.getElementById(${JSON.stringify(id)}).classList.contains('current')};`); + } catch(error) { + report.dialogs[id]={error:String(error),errors:await evaluate('settings','return window.__vulpesDiagnostics;')}; + await evaluate('settings','location.reload();return true;');await sleep(1500); + await wait('settings','return document.readyState==="complete" && typeof require==="function";'); + } + await save(report); + } + report.effects={}; + for(const [name,code] of Object.entries({ + storage:'return await VulpesCompat.call("storage.stats",{});', + modemReadback:'const read=await VulpesCompat.call("platform.mobileSettings",{action:"network"});if(typeof read.enabled!=="boolean")throw Error("Unknown data state");const after=await VulpesCompat.call("platform.mobileSettings",{action:"dataEnabled",enabled:read.enabled});return {same:read.enabled===after.enabled,registered:after.status,technology:after.technology};', + clockReadback:'const read=await VulpesCompat.call("platform.deviceSettings",{action:"clock"});const after=await VulpesCompat.call("platform.deviceSettings",{action:"setTimezone",timezone:read.timezone});return {same:read.timezone===after.timezone,timezone:after.timezone,automaticAvailable:after.automaticAvailable};' + })) { + try {report.effects[name]=await evaluate('settings',code);} + catch(error){report.effects[name]={error:String(error)};} + } + report.networkUi={}; + try { + await evaluate('settings',"window.__vulpesDiagnostics=[];require(['modules/settings_service'],s=>s.navigate('operator-settings'));return true;"); + await wait('settings',"return document.querySelector('#operator-settings.current select:not([disabled])')?.options.length>0;",7000); + report.networkUi.radio=await evaluate('settings',"const s=document.querySelector('#operator-settings select');s.dispatchEvent(new Event('change',{bubbles:true}));return {mode:s.value,choices:Array.from(s.options,o=>o.value)};"); + await wait('settings',"return !document.querySelector('#operator-settings select').disabled;",7000); + report.networkUi.radio.same=await evaluate('settings',`return document.querySelector('#operator-settings select').value===${JSON.stringify(report.networkUi.radio.mode)};`); + for(const type of ['default','mms']) { + await evaluate('settings',`require(['modules/settings_service'],s=>s.navigate('apn-settings'));return true;`); + await wait('settings',"return !!document.querySelector('#apn-settings.current [data-apn-type]');",4000); + await evaluate('settings',`document.querySelector('#apn-settings [data-apn-type=${type}]').click();return true;`); + await wait('settings',"return !!document.querySelector('#apn-list.current label.name');",7000); + await evaluate('settings',"document.querySelector('#apn-list label.name').dispatchEvent(new MouseEvent('mouseup',{bubbles:true}));return true;"); + await wait('settings',"return !!document.querySelector('#apn-editor.current .apn:not([disabled])');",7000); + report.networkUi[type]=await evaluate('settings',"const p=document.querySelector('#apn-editor');return {opened:true,apnPopulated:!!p.querySelector('.apn').value,passwordBlank:p.querySelector('.password').value==='',protocol:p.querySelector('.protocol').value,authentication:p.querySelector('.authtype').value,mmscEditable:!p.querySelector('.mmsc').disabled};"); + // Submit the current APN unchanged, through Gaia. Never change operator + // credentials, activate a bearer or expose any APN/user/password in logs. + await evaluate('settings',"const p=document.querySelector('#apn-editor');p.querySelector('.apn').dispatchEvent(new Event('input',{bubbles:true}));p.querySelector('.ok').click();return true;"); + await wait('settings',"return !!document.querySelector('#apn-list.current label.name');",7000); + report.networkUi[type].savedUnchanged=true; + } + report.networkUi.errors=await evaluate('settings','return window.__vulpesDiagnostics;'); + } catch(error) {report.networkUi.error=String(error);report.networkUi.errors=await evaluate('settings','return window.__vulpesDiagnostics;');} + report.supplementaryUi={}; + try { + await evaluate('settings',"window.__vulpesDiagnostics=[];require(['modules/settings_service'],s=>s.navigate('call'));return true;"); + await wait('settings',"return !!document.querySelector('#call.current #ril-callerId:not([disabled])');",30000); + report.supplementaryUi.calls=await evaluate('settings',"return {waitingEnabled:!document.querySelector('#menuItem-callWaiting gaia-switch').disabled,callerEnabled:!document.querySelector('#ril-callerId').disabled};"); + await evaluate('settings',"document.querySelector('#ril-callerId').dispatchEvent(new Event('change',{bubbles:true}));return true;"); + await wait('settings',"return !document.querySelector('#ril-callerId').disabled;",50000); + report.supplementaryUi.callerSavedUnchanged=true; + await evaluate('settings',"require(['modules/settings_service'],s=>s.navigate('call-forwarding'));return true;"); + await wait('settings',"return !!document.querySelector('#call-forwarding.current .callForwardingUnconditional[aria-disabled=false]');",30000); + report.supplementaryUi.dialogs={}; + for(const [selector,id] of [ + ['.callForwardingUnconditional','call-cf-unconditional-settings'],['.callForwardingMobileBusy','call-cf-mobile-busy-settings'], + ['.callForwardingNoReply','call-cf-no-reply-settings'],['.callForwardingNotReachable','call-cf-not-reachable-settings'] + ]) { + await evaluate('settings',`document.querySelector('#call-forwarding ${selector}').click();return true;`); + await wait('settings',`return document.getElementById('${id}').classList.contains('current');`,4000); + await evaluate('settings',`document.querySelector('#${id} button[type=reset]').click();return true;`); + await wait('settings',"return document.getElementById('call-forwarding').classList.contains('current');",4000); + report.supplementaryUi.dialogs[id]={opened:true,cancelled:true}; + } + report.supplementaryUi.errors=await evaluate('settings','return window.__vulpesDiagnostics;'); + } catch(error) {report.supplementaryUi.error=String(error);report.supplementaryUi.errors=await evaluate('settings','return window.__vulpesDiagnostics;');} + report.hotspotUi={}; + try { + await evaluate('settings',"window.__vulpesDiagnostics=[];require(['modules/settings_service'],s=>s.navigate('hotspot'));return true;"); + await wait('settings',"return !!document.querySelector('#hotspot.current #tethering-wifi-enabled:not([disabled])');",10000); + await evaluate('settings',"document.querySelector('#hotspot-settings-section a').click();return true;"); + await wait('settings',"return document.getElementById('hotspot-wifiSettings').classList.contains('current');",4000); + report.hotspotUi=await evaluate('settings',"const p=document.getElementById('hotspot-wifiSettings');return {opened:true,securityChoices:p.querySelector('select').options.length,passwordMasked:p.querySelector('input[name=password]').type==='password'};"); + await evaluate('settings',"document.querySelector('#hotspot-wifiSettings button[type=reset]').click();return true;"); + await wait('settings',"return document.getElementById('hotspot').classList.contains('current') && !document.getElementById('hotspot-wifiSettings').classList.contains('current') && !document.querySelector('#tethering-wifi-enabled').disabled;",4000); + report.hotspotUi.cancelled=true; + const unconfigured=await evaluate('settings',"const s=await VulpesCompat.call('platform.wifi',{operation:'hotspotStatus'});return !s.configured && !s.enabled;"); + if(unconfigured) { + await evaluate('settings',"document.querySelector('#hotspot-settings-section a').click();return true;"); + await wait('settings',"return document.getElementById('hotspot-wifiSettings').classList.contains('current');",4000); + await evaluate('settings',"const p=document.getElementById('hotspot-wifiSettings');p.querySelector('input[type=text]').value='Vulpes-qualification';p.querySelector('select').value='wpa2-psk';p.querySelector('input[name=password]').value=crypto.randomUUID();p.querySelector('button[type=submit]').click();return true;"); + await wait('settings',"return !!document.querySelector('#hotspot.current #tethering-wifi-enabled:not([disabled])') && !document.getElementById('hotspot-wifiSettings').classList.contains('current');",10000); + report.hotspotUi.configuredByAudit=true; + for(const enabled of [true,false]) { + await evaluate('settings',`const c=document.querySelector('#tethering-wifi-enabled');c.checked=${enabled};c.dispatchEvent(new Event('change',{bubbles:true}));return true;`); + await wait('settings',`const c=document.querySelector('#tethering-wifi-enabled');return !c.disabled && c.checked===${enabled};`,90000); + const actual=await evaluate('settings',"return (await VulpesCompat.call('platform.wifi',{operation:'hotspotStatus'})).enabled;"); + if(actual!==enabled)throw Error('Hotspot UI/readback mismatch'); + report.hotspotUi[enabled?'started':'stopped']=true; + } + } + report.hotspotUi.errors=await evaluate('settings','return window.__vulpesDiagnostics;'); + } catch(error) {report.hotspotUi.error=String(error);report.hotspotUi.errors=await evaluate('settings','return window.__vulpesDiagnostics;');report.hotspotUi.formState=await evaluate('settings',"const p=document.getElementById('hotspot-wifiSettings');return {current:p.classList.contains('current'),message:p.querySelector('[role=status]')?.textContent,submitDisabled:p.querySelector('button[type=submit]')?.disabled};");} + report.scope='panel navigation plus storage read and idempotent data/timezone/radio/APN write/readback; temporary hotspot start/stop when initially unconfigured; no destructive actions, PIN attempts or subscriptions'; + await evaluate('settings','require(["modules/settings_service"],s=>s.navigate("root"));return true;'); + return report; +} diff --git a/host/SettingsFollowupAudit.sys.mjs b/host/SettingsFollowupAudit.sys.mjs new file mode 100644 index 00000000..38357bd5 --- /dev/null +++ b/host/SettingsFollowupAudit.sys.mjs @@ -0,0 +1,56 @@ +// Private qualification entry point, invoked only by a test marker/profile. +export async function auditFollowup({evaluate,wait,sleep,save,tap}) { + const report={checks:{},errors:{},mediaRetained:false}; + const launch=async app=>{ + await evaluate('system',`ScreenManager.turnScreenOn(true);if(Service.query('locked'))await Service.request('unlock',{forcibly:true});await VulpesCompat.call('apps.launch',{manifestURL:'http://${app}.localhost:8765/manifest.webapp'});return true;`); + await wait(app,'return document.readyState==="complete" && !document.hidden;'); + await wait('system',`return Service.query('getActiveApp')?.origin==='http://${app}.localhost:8765';`); + }; + await wait('system',`return document.body?.getAttribute('ready-state')==='fullyLoaded';`,30000); + await sleep(1000); + try { + await launch('settings');await wait('settings','return typeof require==="function";'); + await evaluate('settings',`const s=await new Promise(r=>require(['modules/settings_service'],r));await s.navigate('dateTime');return true;`); + await sleep(800); + // Test the actual Gaia picker without changing the user's system clock. + await evaluate('settings',`if(!navigator.mozTime){window.__testClockInstalled=true;Object.defineProperty(navigator,'mozTime',{value:{},configurable:true});}window.__dateModule=await new Promise(r=>require(['modules/date_time'],r));window.__oldMozTime=__dateModule._mozTime;window.__pickedTime=null;__dateModule._mozTime={set:d=>{window.__pickedTime=+d;return Promise.resolve();}};document.querySelector('#dateTime .time-manual').classList.remove('disabled');for(const e of document.querySelectorAll('#dateTime input'))e.disabled=false;return true;`); + for(const type of ['date','time']) { + await evaluate('settings',`const e=document.querySelector('#dateTime .${type}-picker');e.disabled=false;e.click();return true;`); + await wait('system',`const p=Service.query('getActiveApp')?.valueSelector;return !!(p?.isShown && p._${type}Picker);`); + report.checks[type+'Picker']=await evaluate('system',`const p=Service.query('getActiveApp').valueSelector;const e=p.elements.${type==='date'?'spinDate':'time'}PickerPopup;return {visible:e.checkVisibility(),height:e.getBoundingClientRect().height};`); + await evaluate('system',`Service.query('getActiveApp').valueSelector.confirm();return true;`); + await wait('settings','return Number.isFinite(window.__pickedTime);'); + report.checks[type+'Commit']=true; + await evaluate('settings','window.__pickedTime=null;return true;'); + } + await evaluate('settings',`__dateModule._mozTime=__oldMozTime;const s=await new Promise(r=>require(['modules/settings_service'],r));await s.navigate('keyboard');return true;`); + await launch('keyboard'); + await wait('keyboard','return !!document.querySelector("#general gaia-header");'); + await evaluate('keyboard',`document.querySelector('#general gaia-header').dispatchEvent(new CustomEvent('action'));return true;`); + await wait('system',`return Service.query('getActiveApp')?.origin==='http://settings.localhost:8765';`); + report.checks.keyboardBack=true; + } catch(error){report.errors.settings=String(error);} + finally { + await evaluate('settings','if(window.__dateModule)__dateModule._mozTime=window.__oldMozTime;if(window.__testClockInstalled)delete navigator.mozTime;return true;').catch(()=>{}); + } + await save(report); + try { + await launch('camera'); + await wait('camera','return !!window.app?.camera?.mozCamera && !app.camera.isBusy;'); + await evaluate('camera',`window.__vulpesDiagnostics=[];window.__lastVideo=null;app.once('newmedia',v=>{window.__lastVideo=v;});app.settings.mode.select('video');return true;`); + await wait('camera','return app.camera.mode==="video" && !app.camera.isBusy;'); + await tap('camera','.js-capture'); + await wait('camera','return app.camera.get("recording")==="started";'); + await sleep(3000); + await tap('camera','.js-capture'); + await wait('camera','return !!window.__lastVideo?.blob;',20000); + report.checks.video=await evaluate('camera',`const v=window.__lastVideo;const el=document.createElement('video');el.muted=true;el.src=URL.createObjectURL(v.blob);try{await new Promise((ok,fail)=>{el.onloadeddata=ok;el.onerror=()=>fail(Error('Recorded video cannot be decoded'));});return {bytes:v.blob.size,mime:v.blob.type,path:v.filepath,width:el.videoWidth,height:el.videoHeight,poster:v.poster.blob.size};}finally{URL.revokeObjectURL(el.src);el.remove();}`); + report.errors.camera=await evaluate('camera','return __vulpesDiagnostics.filter(x=>x.kind==="error" || x.kind==="rejection");'); + } catch(error){report.errors.video=String(error);report.errors.camera=await evaluate('camera','return window.__vulpesDiagnostics;').catch(()=>[]);} + finally { + await evaluate('camera',`await app.camera.mozCamera?.vulpesFinishRecording?.();if(window.__lastVideo){await navigator.getDeviceStorage('videos').delete(__lastVideo.filepath);await navigator.getDeviceStorage('pictures').delete(__lastVideo.poster.filepath);}app.settings.mode.select('picture');return true;`).catch(()=>{}); + await evaluate('system',"dispatchEvent(new Event('home'));return true;").catch(()=>{}); + } + report.passed=!Object.values(report.errors).some(x=>Array.isArray(x)?x.length:!!x) && !!report.checks.video; + await save(report);return report; +} diff --git a/host/StorageStats.sys.mjs b/host/StorageStats.sys.mjs new file mode 100644 index 00000000..0c07bd25 --- /dev/null +++ b/host/StorageStats.sys.mjs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MPL-2.0 +// Only byte counts for Vulpes-owned storage cross the application boundary. +let pending, cached; +export async function storageStats(app) { + if(app.id!=='settings')throw Error('PERMISSION_DENIED'); + if(cached && Date.now()-cached.at<5000)return cached.value; + if(pending)return pending; + pending=(async()=>{ + async function bytes(path,skip='') { + if(path===skip || !await IOUtils.exists(path))return 0; + const file=Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile);file.initWithPath(path); + if(file.isSymlink())return 0; + const info=await IOUtils.stat(path); + if(info.type==='regular')return info.size; + if(info.type!=='directory')return 0; + let total=0; + for(const child of await IOUtils.getChildren(path)) { + try {total+=await bytes(child,skip);} catch(error) {if(error.name!=='NotFoundError')throw error;} + } + return total; + } + const profile=PathUtils.profileDir,media=PathUtils.join(profile,'vulpes-media'); + const file=Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile);file.initWithPath(profile); + const value={free:file.diskSpaceAvailable,apps:await bytes(profile,media),sdcard:await bytes(media),scope:'vulpes-profile'}; + cached={at:Date.now(),value};return value; + })().finally(()=>{pending=null;}); + return pending; +} diff --git a/host/Tundra.sys.mjs b/host/Tundra.sys.mjs index a6667461..3c5f29e9 100644 --- a/host/Tundra.sys.mjs +++ b/host/Tundra.sys.mjs @@ -42,6 +42,7 @@ export function tundraCapabilities(release) { dial: radio, callControl: radio, sms: radio, + mms: radio, alarms: false, hardwareStatus: true, wifiControl: Services.env?.get('VULPES_TUNDRA_WIFI') === '1', @@ -50,7 +51,7 @@ export function tundraCapabilities(release) { export async function tundraRadio(operation, params = {}) { if (Services.env.get('VULPES_TUNDRA_RADIO') !== '1') throw Error('RADIO_UNAVAILABLE'); - if (!['status', 'calls', 'dial', 'hangup', 'answer', 'sms', 'incomingMessages', 'ackMessages', 'messageState'].includes(operation)) throw Error('RADIO_INVALID_REQUEST'); + if (!['deviceSettings', 'mobileSettings', 'status', 'calls', 'dial', 'hangup', 'answer', 'sms', 'incomingMessages', 'ackMessages', 'messageState'].includes(operation)) throw Error('RADIO_INVALID_REQUEST'); const { Subprocess } = ChromeUtils.importESModule('resource://gre/modules/Subprocess.sys.mjs'); const process = await Subprocess.call({ command: '/usr/bin/python3', @@ -78,8 +79,8 @@ export async function tundraRadio(operation, params = {}) { throw Error('RADIO_INVALID_REPLY'); } if (reply?.ok === false) { - const errors = ['RADIO_NOT_REGISTERED', 'RADIO_BUSY', 'RADIO_NOT_READY', - 'RADIO_REQUEST_FAILED', 'INVALID_NUMBER', 'INVALID_SMS', 'INVALID_CALL']; + const errors = ['RADIO_NOT_REGISTERED', 'RADIO_BUSY', 'RADIO_NOT_READY', 'RADIO_TIMEOUT', + 'RADIO_REQUEST_FAILED', 'RADIO_READBACK_FAILED', 'INVALID_REQUEST', 'APN_IN_USE', 'APN_NOT_FOUND', 'APN_WRITE_FAILED', 'APN_ROLLBACK_FAILED', 'CLOCK_REQUEST_FAILED', 'CLOCK_UNAVAILABLE', 'INVALID_NUMBER', 'INVALID_SMS', 'INVALID_CALL']; throw Error(errors.includes(reply.error) ? reply.error : 'RADIO_REQUEST_FAILED'); } if (reply?.ok !== true) throw Error('RADIO_INVALID_REPLY'); @@ -92,11 +93,11 @@ const WIFI_SCAN_CACHE_MS = 5000; export async function tundraWifi(params) { if (Services.env.get('VULPES_TUNDRA_WIFI') !== '1') throw Error('NOT_SUPPORTED'); - if (!['scan', 'connect', 'disconnect', 'status', 'setEnabled', 'known', 'forget'].includes(params.operation)) + if (!['scan', 'connect', 'connectHidden', 'disconnect', 'status', 'setEnabled', 'known', 'forget','hotspotStatus','hotspotSave','hotspotPower'].includes(params.operation)) throw Error('WIFI_INVALID_REQUEST'); if (params.operation !== 'scan') { try { return await wifiCommand(params); } - finally { wifiScanCache = null; } + finally { if (['connect','connectHidden','disconnect','forget','setEnabled','hotspotPower'].includes(params.operation)) wifiScanCache = null; } } // System and Settings share a broker: at most one Python scan client exists. if (wifiScanPending) return wifiScanPending; @@ -176,3 +177,33 @@ export async function tundraCallAudio(params) { if (reply.error) throw Error(reply.error); return reply.result; } + + +// Separate socket and bounded payload so MMS traffic cannot block call control. +export async function tundraMms(operation, params={}) { + if(Services.env.get('VULPES_TUNDRA_RADIO')!=='1') throw Error('NotSupportedError'); + if(!['send','state','incoming','ack'].includes(operation)) throw Error('INVALID_REQUEST'); + const {Subprocess}=ChromeUtils.importESModule('resource://gre/modules/Subprocess.sys.mjs'); + const process=await Subprocess.call({command:'/usr/bin/python3',arguments:['/etc/tundra/mms-control.py','--client'],stderr:'ignore'}); + await process.stdin.write(JSON.stringify({version:1,operation,...params}));await process.stdin.close(); + let output='';for(;;){const chunk=await process.stdout.readString();if(!chunk)break;output+=chunk;if(output.length>1228800){process.kill();throw Error('MMS_TOO_LARGE');}} + const {exitCode}=await process.wait();if(exitCode)throw Error('MMS_UNAVAILABLE'); + const reply=JSON.parse(output);if(reply.ok!==true)throw Error(reply.error || 'MMS_UNAVAILABLE');return reply.result; +} + +// A persistent daemon owns BlueZ discovery and pending pairing requests. +export async function tundraBluetooth(params) { + if (Services.env.get('VULPES_TUNDRA') !== '1') throw Error('NotSupportedError'); + const {Subprocess}=ChromeUtils.importESModule('resource://gre/modules/Subprocess.sys.mjs'); + const process=await Subprocess.call({command:'/usr/bin/python3', + arguments:['/etc/tundra/bluetooth-control.py','--client'],stderr:'ignore'}); + await process.stdin.write(JSON.stringify({...params,version:1}));await process.stdin.close(); + let output=''; + for (;;) { + const chunk=await process.stdout.readString();if(!chunk)break; + output+=chunk;if(output.length>65536){process.kill();throw Error('BLUETOOTH_INVALID_REPLY');} + } + const {exitCode}=await process.wait();if(exitCode)throw Error('BLUETOOTH_UNAVAILABLE'); + const reply=JSON.parse(output);if(reply.error)throw Error(reply.error); + return reply.result; +} diff --git a/host/actors/VulpesChild.sys.mjs b/host/actors/VulpesChild.sys.mjs index c69ed27f..960dd01f 100644 --- a/host/actors/VulpesChild.sys.mjs +++ b/host/actors/VulpesChild.sys.mjs @@ -1,16 +1,20 @@ +import {InputEditor} from 'resource://vulpes/services/input-editor.mjs'; export class VulpesChild extends JSWindowActorChild { handleEvent(event) { - if(['focusin','pointerdown'].includes(event.type)) { - if(this.editing) return; + if(['focusin','pointerdown','click'].includes(event.type)) { + if(this._editor?.editing) return; if(!Services.cpmm.sharedData.get('vulpes:native-input')) return; let element=event.target; if(element?.isContentEditable) element=element.closest('[contenteditable=true]') || element; const editable=element?.isContentEditable || (['input','textarea'].includes(element?.localName) && !['button','submit','range','checkbox','radio','date','time','datetime-local','file'].includes(element.type)); if(!editable || element.disabled || element.readOnly) { - if(event.type==='pointerdown') this.sendAsyncMessage('Vulpes:InputFocus',null); + // Dismissing on pointerdown moves buttons before pointerup and loses + // the click. Composer actions may also explicitly retain the editor. + if(event.type==='click' && !element?.closest?.('[data-vulpes-keep-input]')) + this.sendAsyncMessage('Vulpes:InputFocus',null); return; } - this.inputElement=element; + this.editor.focus(element); this.sendAsyncMessage('Vulpes:InputFocus',this.inputState()); return; } @@ -76,69 +80,9 @@ export class VulpesChild extends JSWindowActorChild { { defineAs: 'vulpesFrameForContext' }, ); } - inputState() { - const e=this.inputElement; - const text=e.isContentEditable ? e.textContent : e.value; - const selection=this.contentWindow.getSelection(); - let start=e.selectionStart ?? text.length, end=e.selectionEnd ?? start; - if(e.isContentEditable && selection.rangeCount && e.contains(selection.anchorNode)) { - const range=this.document.createRange();range.selectNodeContents(e); - range.setEnd(selection.anchorNode,selection.anchorOffset);start=range.toString().length; - range.setEnd(selection.focusNode,selection.focusOffset);end=range.toString().length; - [start,end]=[Math.min(start,end),Math.max(start,end)]; - } - return {type:e.isContentEditable?'contenteditable':e.localName,inputType:e.type || (e.classList.contains('recipient')?'tel':'text'), - inputMode:e.inputMode || (e.classList.contains('recipient')?'tel':''), - text:e.type==='password'?'':text,selectionStart:start,selectionEnd:end}; - } - edit(p) { - const e=this.inputElement,win=this.contentWindow; - if(!e?.isConnected || this.document.hidden) throw Error('INPUT_CONTEXT_EXPIRED'); - this.editing=true; - try {e.focus();} finally {this.editing=false;} - // A touch can focus a new contenteditable while Gecko retains the previous - // field's selection. execCommand follows that selection, not activeElement. - if(e.isContentEditable) { - const selection=win.getSelection(); - if(!e.contains(selection.anchorNode) || !e.contains(selection.focusNode)) { - const range=this.document.createRange(); - range.selectNodeContents(e);range.collapse(false); - selection.removeAllRanges();selection.addRange(range); - } - } - if(p.operation==='key') { - let key=typeof p.key==='object'?p.key.key:p.key; - if(key===8 || key==='Backspace') this.document.execCommand('delete'); - else if(key===13 || key==='Enter') { - const options={key:'Enter',code:'Enter',keyCode:13,charCode:13,which:13,bubbles:true,cancelable:true}; - const accepted=e.dispatchEvent(new win.KeyboardEvent('keydown',options)) && e.dispatchEvent(new win.KeyboardEvent('keypress',options)); - if(accepted && (e.isContentEditable || e.localName==='textarea')) this.document.execCommand('insertLineBreak'); - e.dispatchEvent(new win.KeyboardEvent('keyup',options)); - } else { - const text=typeof key==='string'?key:String.fromCodePoint(p.charCode || key); - if(text.length>16) throw Error('INVALID_VALUE'); - this.document.execCommand('insertText',false,text); - } - } else if(['selection','replace'].includes(p.operation)) { - const state=this.inputState(); - const start=p.operation==='selection'?p.start:state.selectionStart+(p.offset||0); - const length=p.length||0; - if(!Number.isInteger(start)||!Number.isInteger(length)||start<0||length<0||start+length>(e.value ?? e.textContent).length) throw Error('INVALID_VALUE'); - if(e.isContentEditable) { - const walker=this.document.createTreeWalker(e,win.NodeFilter.SHOW_TEXT); - const points=[];let offset=0,node; - while((node=walker.nextNode())) {points.push({node,start:offset,end:offset+node.length});offset+=node.length;} - const point=index=>{const part=points.find(p=>index<=p.end);return part?[part.node,index-part.start]:[e,0];}; - const range=this.document.createRange();range.setStart(...point(start));range.setEnd(...point(start+length)); - win.getSelection().removeAllRanges();win.getSelection().addRange(range); - } else e.setSelectionRange(start,start+length); - if(p.operation==='replace') { - if(typeof p.text!=='string'||p.text.length>4096) throw Error('INVALID_VALUE'); - this.document.execCommand('insertText',false,p.text); - } - } else throw Error('METHOD_NOT_SUPPORTED'); - return this.inputState(); - } + get editor() { return this._editor ||= new InputEditor(this.contentWindow); } + inputState() { return this.editor.state(); } + edit(params) { return this.editor.edit(params); } receiveMessage(message) { if(message.name==='Vulpes:Edit') return this.edit(message.data); if (message.name === 'Vulpes:ViewState') { @@ -146,9 +90,16 @@ export class VulpesChild extends JSWindowActorChild { f => f.browsingContext?.id === message.data.contextId); if (!frame?.isConnected) return {visible:false}; const win = this.contentWindow, rect = frame.getBoundingClientRect(); + // App chrome (the status bar and browser controls) legitimately covers + // the corners of a mozbrowser iframe. Requiring every corner to hit the + // iframe hid the external browser completely, leaving a white page for + // sites such as YouTube. The centre plus one side sample is sufficient + // to prove that the content viewport is visible. + const samples = [[.5,.5],[.05,.5],[.95,.5],[.5,.05],[.5,.95]]; + const hits = samples.reduce((count,[x,y]) => count + + (this.document.elementFromPoint(rect.left+rect.width*x,rect.top+rect.height*y)===frame ? 1 : 0), 0); const visible = !this.document.hidden && frame.checkVisibility({checkOpacity:true,checkVisibilityCSS:true}) && - [[.5,.5],[.05,.05],[.95,.05],[.05,.95],[.95,.95]].every(([x,y]) => - this.document.elementFromPoint(rect.left+rect.width*x,rect.top+rect.height*y)===frame); + hits >= 2; let top = Math.max(0,rect.top); // Gaia's status bar belongs above all app content, including native web views. if (this.document.location.hostname === 'system.localhost') { diff --git a/host/desktop.css b/host/desktop.css index 40a36351..2f92504c 100644 --- a/host/desktop.css +++ b/host/desktop.css @@ -146,6 +146,12 @@ h1, h2, h3, h4, h5, h6, #statusbar-time, /* Gaia owns edge gestures; avoid a second host overscroll effect. */ html[data-vulpes-app] { overscroll-behavior:none; } +/* padding-box is no longer supported. Content-box made the full-width + section wrap below the dialog's spacer, hiding its title and message. */ +form[role='dialog'][data-type='confirm'] > section { + box-sizing: border-box; +} + /* Native selects now paint their own arrow; the old Gecko arrow-hiding offset clips Fira descenders on current Gecko. */ @@ -200,3 +206,47 @@ html[data-vulpes-app='camera'], html[data-vulpes-app='camera'] body { touch-acti /* Homescreen pages must settle at their boundary after a drag (Forgejo #3). */ html[data-vulpes-app="homescreen"] #panels { scroll-snap-type:x mandatory; } html[data-vulpes-app="homescreen"] #panels > * { scroll-snap-align:start; } + +/* System gestures are handled by Gaia, not by the page's pan/zoom handler. + App frames keep their own scrolling; the notification list scrolls vertically. */ +html[data-vulpes-app='system'], +html[data-vulpes-app='system'] body { + overscroll-behavior: none; +} +html[data-vulpes-app='system'] #lockscreen { + touch-action: none; +} +html[data-vulpes-app='system'] #utility-tray { + touch-action: pan-y; + overscroll-behavior: contain; +} + +/* Keep the original Gaia value selector inside the application's viewport. */ +html[data-vulpes-app] #vulpes-value-selector { + position: fixed; inset: 0; z-index: 2147483000; margin: 0; + font-family: FiraSans, sans-serif; box-sizing: border-box; +} +html[data-vulpes-app] #vulpes-value-selector > section { box-sizing: border-box; } +html[data-vulpes-app] #vulpes-value-selector [role=option][aria-disabled=true] { opacity: .45; } + +html[data-vulpes-app] #vulpes-value-selector-layer { + position: fixed; inset: 0; z-index: 2147483000; +} + +html[data-vulpes-app] :is(.button,.bb-button) select.vulpes-select-label-owner { + opacity: 0; +} +html[data-vulpes-app] .vulpes-select-label { + position: absolute; + inset: 0 3.5rem 0 1.1rem; + display: flex; + align-items: center; + pointer-events: none; + font: italic 1.7rem/1.5 FiraSans, sans-serif; + white-space: nowrap; + overflow: hidden; + color: var(--text-color, #333); + text-shadow: none; +} +html[dir=rtl][data-vulpes-app] .vulpes-select-label { inset: 0 1.1rem 0 3.5rem; } +html[data-vulpes-theme=dark] .vulpes-select-label { color: #eee; } diff --git a/host/gaia-bluetooth.js b/host/gaia-bluetooth.js new file mode 100644 index 00000000..cc848e55 --- /dev/null +++ b/host/gaia-bluetooth.js @@ -0,0 +1,126 @@ +// Gaia Bluetooth v2, backed by the host's permission-checked BlueZ broker. +(() => { + const app = location.hostname.split('.')[0]; + if (!(window.VulpesPlatform === 'tundra' || window.VulpesTestBluetooth) || + !['system', 'settings', 'bluetooth'].includes(app)) return; + const {call} = VulpesCompat; + const manager = Object.assign(new EventTarget(), {defaultAdapter:null}); + const adapter = Object.assign(new EventTarget(), { + state:'disabled', address:'', name:'', discoverable:false, discovering:false, + pairingReqs:new EventTarget(), + }); + const devices = new Map(); + let discovery = null, seen = new Set(), pending = false, timer, closed = false; + let currentPairing = null, initialized = false; + const emit = (target, type, values={}) => { + const event = Object.assign(new Event(type), values); + target.dispatchEvent(event); target['on'+type]?.(event); + }; + function change(target, fields) { + const attrs = Object.keys(fields).filter(k=>JSON.stringify(target[k])!==JSON.stringify(fields[k])); + Object.assign(target, fields); + if (attrs.length) emit(target, 'attributechanged', {attrs}); + } + function apply(state) { + if (!state?.available) return; + for (const row of state.devices) { + let device = devices.get(row.address); + const oldPaired = device?.paired; + if (!device) { + device = new EventTarget(); devices.set(row.address, device); + } + change(device, {...row, connectionStatus:row.connected?'connected':'disconnected'}); + if (oldPaired !== undefined && oldPaired !== row.paired) + emit(adapter, row.paired?'devicepaired':'deviceunpaired', {device,address:row.address}); + if (discovery && state.discovering && !seen.has(row.address)) { + seen.add(row.address); emit(discovery, 'devicefound', {device}); + } + } + for (const [address, device] of devices) { + if (!state.devices.some(d=>d.address===address)) { + if (device.paired) emit(adapter,'deviceunpaired',{address,device}); + devices.delete(address); + } + } + change(adapter, {state:state.enabled?'enabled':'disabled',address:state.address, + name:state.name,discoverable:state.discoverable,discovering:state.discovering}); + if (!manager.defaultAdapter) { + manager.defaultAdapter=adapter; emit(manager,'attributechanged',{attrs:['defaultAdapter']}); + } + currentPairing = state.pairing?.[0] || null; + if (app==='bluetooth' && location.pathname==='/onpair.html') showPairing(); + } + async function command(operation, params={}) { + const value=await call('platform.bluetooth',{operation,...params}); + if (value?.available) apply(value); + return value; + } + async function refresh() { + if (pending || closed) return; + pending=true; + try {apply(await command('status'));} + catch(error) {console.warn('Bluetooth:',error.message);} + finally {pending=false;} + } + adapter.enable=()=>command('power',{enabled:true}); + adapter.disable=()=>command('power',{enabled:false}); + adapter.setName=name=>command('rename',{name}); + adapter.setDiscoverable=enabled=>command('discoverable',{enabled}); + adapter.startDiscovery=async()=>{ + await command('scan'); + discovery=new EventTarget(); seen=new Set(); + const handle=discovery; + // Gaia installs its discovery listeners after this promise resolves. + setTimeout(()=>refresh(),0); + return handle; + }; + adapter.stopDiscovery=async()=>{await command('stopScan');discovery=null;}; + adapter.getPairedDevices=()=>[...devices.values()].filter(d=>d.paired); + adapter.getConnectedDevices=async profile=>{ + await refresh(); + const uuid=Number(profile).toString(16).padStart(8,'0')+'-0000-1000-8000-00805f9b34fb'; + return [...devices.values()].filter(d=>d.connected && d.uuids.includes(uuid)); + }; + for (const op of ['pair','unpair','connect','disconnect']) + adapter[op]=async value=>{ + await command(op,{address:typeof value==='string'?value:value.address}); + await refresh(); + }; + const unsupported=()=>Promise.reject(new DOMException('Bluetooth profile is unavailable','NotSupportedError')); + for (const name of ['sendFile','stopSendingFile','confirmReceivingFile','connectSco','disconnectSco', + 'sendMediaMetaData','sendMediaPlayStatus']) adapter[name]=unsupported; + adapter.isScoConnected=()=>Promise.resolve(false); + manager.getAdapters=()=>Promise.resolve(manager.defaultAdapter?[adapter]:[]); + Object.defineProperty(navigator,'mozBluetooth',{value:manager}); + + let shown = null; + function showPairing() { + if (!initialized || !window.Pairview) return; + const item=currentPairing; + if (!item) {if(shown) window.close();return;} + if (shown===item.id) return; + shown=item.id; + const reply=(accept,code)=>command('reply',{id:item.id,accept,code}); + Pairview.init(item.method==='enterpasskey'?'enterpincode':item.method,{ + deviceName:item.deviceName,handle:{passkey:item.passkey, + accept:()=>reply(true),reject:()=>reply(false),setPinCode:code=>reply(true,code)}, + }); + } + addEventListener('DOMContentLoaded',async()=>{ + initialized=true; + await refresh(); showPairing(); + if(app==='system') { + navigator.mozSettings.addObserver('bluetooth.enabled',event=>{ + const enabled=event.settingValue===true; + if((adapter.state==='enabled')!==enabled) + command('power',{enabled}).catch(()=>call('settings.set',{values:{'bluetooth.enabled':adapter.state==='enabled'}})); + }); + await call('settings.set',{values:{'bluetooth.enabled':adapter.state==='enabled'}}); + } + timer=setInterval(()=>{if(app==='system'||!document.hidden||discovery)refresh();},1500); + },{once:true}); + addEventListener('pagehide',()=>{ + closed=true;clearInterval(timer); + if(discovery)command('stopScan').catch(()=>{}); + },{once:true}); +})(); diff --git a/host/gaia-camera.js b/host/gaia-camera.js index b056ff2d..63b46b0c 100644 --- a/host/gaia-camera.js +++ b/host/gaia-camera.js @@ -1,6 +1,8 @@ // The Gaia camera UI consumes mozCameras. Only live MediaStream capabilities are exposed here. (() => { if (location.hostname !== 'camera.localhost') return; + const videoMime = typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported('video/webm;codecs=vp8,opus') ? 'video/webm;codecs=vp8,opus' : null; + window.VulpesVideoExtension = videoMime ? 'webm' : null; let cameras = ['back'], devices = new Map(), current = null, @@ -8,11 +10,22 @@ let cameraError = null; let opening = Promise.resolve(), closing = Promise.resolve(); const releases = new WeakMap(); + const nativeViews = new WeakMap(); function releaseStream(stream, preview) { if (releases.has(stream)) return releases.get(stream); if (current === stream) current = null; const release = Promise.resolve().then(async () => { + await stream.vulpesFinishRecording?.(); + emit(stream, 'previewstatechange', {newState:'stopped'}); + // Stop the capture producer before detaching its consumers. Gecko can + // otherwise block in srcObject=null while a GPU-backed frame is in use. + stream.getTracks().forEach(track => track.stop()); for (const video of document.querySelectorAll('video')) { + if (nativeViews.get(video) === stream) { + nativeViews.delete(video); + stream.vulpesPreviewCanvas.remove(); + video.style.display = ''; + } if (video.srcObject === stream) { video.pause(); video.srcObject = null; if ('vulpesCapture' in video.dataset) video.remove(); @@ -96,12 +109,33 @@ Object.defineProperty(HTMLMediaElement.prototype, 'mozSrcObject', { configurable: true, get() { - return nativeSource.get.call(this); + return nativeViews.get(this) || nativeSource.get.call(this); }, set(stream) { - nativeSource.set.call(this, stream); + const old = nativeViews.get(this); + if (old) { + old.vulpesPreviewCanvas.remove(); + nativeViews.delete(this); + this.style.display = ''; + } + if (stream?.vulpesPreviewCanvas) { + // The HAL already delivers NV21 frames. Render them directly in Gaia's + // viewfinder instead of capturing the WebGL canvas into a second GPU + // video pipeline, which stalls on repeated switches on Sargo. + nativeViews.set(this, stream); + const canvas = stream.vulpesPreviewCanvas; + canvas.className = this.className; + canvas.dataset.vulpesNativePreview = ''; + canvas.style.cssText = 'display:block;width:100%;height:100%;pointer-events:none'; + this.style.display = 'none'; + this.parentNode.append(canvas); + } else nativeSource.set.call(this, stream); }, }); + const playVideo = HTMLMediaElement.prototype.play; + HTMLMediaElement.prototype.play = function() { + return nativeViews.has(this) ? Promise.resolve() : playVideo.call(this); + }; function emit(target, type, detail = {}) { const event = new Event(type); Object.assign(event, detail); @@ -136,15 +170,20 @@ if (second) devices.set('front', second.deviceId); cameras = devices.size ? [...devices.keys()] : ['back']; } + // Keep one graphics context per app. Losing/recreating it at every lens switch + // can exhaust the mobile driver while captured video frames are still in use. + let nativeCanvas = null; async function nativeStream(name) { let session; const call = (params) => VulpesCompat.call('platform.cameraPreview', {...params, session}); const meta = await call({action:'start', camera:name === 'front' ? 'front' : 'back'}); session = meta.session; - const canvas = document.createElement('canvas'); + const canvas = nativeCanvas || (nativeCanvas = document.createElement('canvas')); const sideways = meta.orientation % 180 !== 0; - canvas.width = sideways ? meta.height : meta.width; - canvas.height = sideways ? meta.width : meta.height; + const width = sideways ? meta.height : meta.width; + const height = sideways ? meta.width : meta.height; + if (canvas.width !== width) canvas.width = width; + if (canvas.height !== height) canvas.height = height; // Convert NV21 in a shader instead of blocking Gaia's UI with one JS loop // per pixel. The two textures contain Y and interleaved V/U respectively. const gl = canvas.getContext('webgl', {alpha:false, antialias:false, preserveDrawingBuffer:true}); @@ -152,18 +191,21 @@ await call({action:'stop'}); throw new DOMException('Camera preview requires WebGL', 'NotSupportedError'); } - const timing = {frames:0, startedAt:performance.now(), conversionMs:0}; - let stopped = false, timer, stream, sequence = -1, closing, program, buffer; + const timing = {frames:0, startedAt:performance.now(), conversionMs:0, transferMs:0, sensorFrames:0}; + let stopped = false, timer, stream, wakeLock, sequence = -1, closing, program, buffer; const textures = [], shaders = []; const stop = () => { if (closing) return closing; stopped = true; clearTimeout(timer); + wakeLock?.unlock(); wakeLock = null; closing = call({action:'stop'}).finally(() => { + if (stream) stream.vulpesClosed = true; textures.forEach(texture => gl.deleteTexture(texture)); shaders.forEach(value => gl.deleteShader(value)); if (buffer) gl.deleteBuffer(buffer); if (program) gl.deleteProgram(program); - gl.getExtension('WEBGL_lose_context')?.loseContext(); + // The next lens reuses this context after its old stream has stopped. + gl.clearColor(0,0,0,1); gl.clear(gl.COLOR_BUFFER_BIT); }); return closing; }; @@ -210,17 +252,25 @@ gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE); gl.uniform1i(gl.getUniformLocation(program,name),index); }); + gl.viewport(0,0,canvas.width,canvas.height); + const pixels = meta.width * meta.height; + gl.activeTexture(gl.TEXTURE0); + gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,meta.width,meta.height,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,null); + gl.activeTexture(gl.TEXTURE1); + gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE_ALPHA,meta.width/2,meta.height/2,0,gl.LUMINANCE_ALPHA,gl.UNSIGNED_BYTE,null); const draw = async () => { + const requested = performance.now(); const frame = await call({action:'frame'}); + timing.transferMs += performance.now()-requested; if (stopped || !frame.data || frame.sequence === sequence) return false; const data = new Uint8Array(frame.data), w = meta.width, h = meta.height; if (data.length !== w * h * 3 / 2) throw Error('Invalid native camera frame'); - sequence = frame.sequence; + sequence = frame.sequence; timing.sensorFrames = sequence; const start=performance.now(); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,textures[0]); - gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,w,h,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,data.subarray(0,w*h)); + gl.texSubImage2D(gl.TEXTURE_2D,0,0,0,w,h,gl.LUMINANCE,gl.UNSIGNED_BYTE,data.subarray(0,pixels)); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D,textures[1]); - gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE_ALPHA,w/2,h/2,0,gl.LUMINANCE_ALPHA,gl.UNSIGNED_BYTE,data.subarray(w*h)); + gl.texSubImage2D(gl.TEXTURE_2D,0,0,0,w/2,h/2,gl.LUMINANCE_ALPHA,gl.UNSIGNED_BYTE,data.subarray(pixels)); gl.drawArrays(gl.TRIANGLE_STRIP,0,4); timing.frames++;timing.conversionMs+=performance.now()-start; return true; @@ -230,7 +280,11 @@ if (Date.now() > deadline) throw new DOMException('No camera frames', 'NotReadableError'); await new Promise(resolve => setTimeout(resolve, 50)); } - stream = canvas.captureStream(24); + stream = new MediaStream(); + stream.vulpesPreviewCanvas = canvas; + stream.vulpesClosed = false; + // A canvas has no video element's implicit screen wake lock. + wakeLock = navigator.requestWakeLock?.('screen'); stream.vulpesTiming = timing; stream.vulpesNative = true; stream.vulpesCameraMetadata = meta; @@ -245,15 +299,28 @@ const started = performance.now(); try { await draw(); } catch (error) { - if (!stopped) { recordError(error); stream.getTracks().forEach(track => track.stop()); } + if (!stopped) { recordError(error); await stop().catch(recordError); } return; } - if (!stopped) timer = setTimeout(next, Math.max(0, 1000/24-(performance.now()-started))); + if (!stopped) timer = setTimeout(next, Math.max(0, 1000/30-(performance.now()-started))); }; - timer = setTimeout(next, 1000/24); + timer = setTimeout(next, 1000/30); return stream; } catch (error) { await stop().catch(() => {}); throw error; } } + async function captureVideo(constraints, ticket, platform) { + // track.stop() returns before some Android camera drivers finish closing. + // Retry the same sensor, never replace a refused/missing camera silently. + for (let attempt = 0; ; attempt++) { + if (document.hidden || ticket !== generation) + throw new DOMException('Camera request superseded or hidden', 'AbortError'); + try { return await navigator.mediaDevices.getUserMedia(constraints); } + catch (error) { + if (platform !== 'android' || error.name !== 'NotReadableError' || attempt >= 2) throw error; + await new Promise(resolve => setTimeout(resolve, 200 * (attempt + 1))); + } + } + } function getCamera(name, config = {}) { const ticket = ++generation; const request = opening.then(() => openCamera(name, config, ticket)); @@ -275,7 +342,7 @@ const platform = await VulpesCompat.call('platform.capabilities', {}); if (platform.platform === 'tundra' && !platform.camera) throw new DOMException('Native camera adapter unavailable', 'NotSupportedError'); - stream = platform.platform === 'tundra' ? await nativeStream(name) : await navigator.mediaDevices.getUserMedia({ + stream = platform.platform === 'tundra' ? await nativeStream(name) : await captureVideo({ audio: false, video: { ...(selected @@ -284,7 +351,7 @@ width: { ideal: 1920 }, height: { ideal: 1080 }, }, - }); + }, ticket, platform.platform); } catch (error) { recordError(error); throw error; @@ -304,44 +371,55 @@ throw new DOMException('Camera hidden', 'AbortError'); const track = stream.getVideoTracks()[0]; - const settings = track.getSettings(); - const size = { width: settings.width, height: settings.height }; - preview = document.createElement('video'); - preview.muted = true; - preview.playsInline = true; - preview.srcObject = stream; - preview.dataset.vulpesCapture = ''; - preview.style.cssText = 'position:fixed;width:1px;height:1px;opacity:0;pointer-events:none'; - document.body.append(preview); - let previewTimeout; - await Promise.race([ - preview.play(), - new Promise((_, reject) => { - previewTimeout = setTimeout( - () => reject(new DOMException('Camera preview timed out', 'NotReadableError')), - 10000, - ); - }), - ]).finally(() => clearTimeout(previewTimeout)); + const native = stream.vulpesCameraMetadata; + let size; + if (native) { + // Preview dimensions and still photos come directly from the HAL. + // The native canvas needs no hidden capture video. + const sideways = native.orientation % 180 !== 0; + size = {width:sideways ? native.height : native.width, + height:sideways ? native.width : native.height}; + } else { + preview = document.createElement('video'); + preview.muted = true; + preview.playsInline = true; + preview.srcObject = stream; + preview.dataset.vulpesCapture = ''; + preview.style.cssText = 'position:fixed;width:1px;height:1px;opacity:0;pointer-events:none'; + document.body.append(preview); + let previewTimeout; + await Promise.race([ + preview.play(), + new Promise((_, reject) => { + previewTimeout = setTimeout( + () => reject(new DOMException('Camera preview timed out', 'NotReadableError')), + 10000, + ); + }), + ]).finally(() => clearTimeout(previewTimeout)); + const settings = stream.getVideoTracks()[0].getSettings(); + size = {width:preview.videoWidth || settings.width, + height:preview.videoHeight || settings.height}; + } if (document.hidden || ticket !== generation) throw new DOMException('Camera hidden', 'AbortError'); - const native = stream.vulpesCameraMetadata; const pictureSizes = native?.pictureSizes || [size]; let pictureSize = pictureSizes.reduce((a,b)=>a.width*a.height>b.width*b.height?a:b); - let focusArea = null, zoom = 1; + let focusArea = null, zoom = 1, mode = 'picture', recording, startingRecording = false; + const profile = {containerFormat:'webm',video:{width:size.width,height:size.height,codec:'vp8',bitrate:1500000,framerate:30},audio:{codec:'opus',samplerate:48000,channels:1,bitrate:64000}}; const configuration = () => ({ - mode: 'picture', + mode, previewSize: size, pictureSize, - recorderProfile: null, + recorderProfile: videoMime ? 'webm' : null, }); Object.assign(stream, { capabilities: { pictureSizes, previewSizes: [size], thumbnailSizes: [], - recorderProfiles: {}, + recorderProfiles: videoMime ? {webm:profile} : {}, flashModes: ['off'], focusModes: native ? (native.focusModes.includes('auto') ? ['auto','infinity'] : ['fixed']) : ['fixed'], sceneModes: ['auto'], @@ -359,8 +437,9 @@ zoom: 1, async setConfiguration(value) { if (value.pictureSize) stream.setPictureSize(value.pictureSize); - if (value.mode && value.mode !== 'picture') + if (value.mode && !['picture',...(videoMime ? ['video'] : [])].includes(value.mode)) throw new DOMException('Video unavailable', 'NotSupportedError'); + if(value.mode)mode=value.mode; queueMicrotask(() => emit(stream, 'previewstatechange', { newState: 'started' })); return configuration(); }, @@ -415,10 +494,66 @@ release() { return releaseStream(stream, preview); }, - async startRecording() { - throw new DOMException('Video unavailable', 'NotSupportedError'); + async startRecording(options, storage, filepath) { + if(!videoMime)throw new DOMException('Video unavailable','NotSupportedError'); + if(recording || startingRecording)throw new DOMException('Recording in progress','InvalidStateError'); + startingRecording=true; + let audio, captured; + try { + audio=await navigator.mediaDevices.getUserMedia({audio:true,video:false}); + if(current!==stream || document.hidden)throw new DOMException('Camera closed','AbortError'); + // Only recording captures the native canvas. The preview keeps its + // direct WebGL surface and the capture track is stopped before a switch. + captured=native ? stream.vulpesPreviewCanvas.captureStream(30) : new MediaStream(stream.getVideoTracks().map(t=>t.clone())); + for(const track of audio.getAudioTracks())captured.addTrack(track); + const recorder=new MediaRecorder(captured,{mimeType:videoMime,videoBitsPerSecond:1500000,audioBitsPerSecond:64000}); + const chunks=[];let bytes=0,failed=false,finish; + const done=new Promise(resolve=>{finish=resolve;}); + const cap=Math.min(options.maxFileSizeBytes || Infinity,32*1024*1024); + recorder.ondataavailable=event=>{ + if(event.data.size) {chunks.push(event.data);bytes+=event.data.size;} + if(bytes>=cap && recorder.state==='recording') { + emit(stream,'recorderstatechange',{newState:'FileSizeLimitReached'}); + recorder.stop(); + } + }; + recorder.onerror=()=>{failed=true;emit(stream,'recorderstatechange',{newState:'MediaRecorderFailed'});}; + recorder.onstop=async()=>{ + try { + if(failed || !bytes)throw Error('Empty or failed recording'); + const blob=new Blob(chunks,{type:'video/webm'}); + await storage.addNamed(blob,filepath); + const canvas=document.createElement('canvas'); + const source=native ? stream.vulpesPreviewCanvas : preview; + canvas.width=native ? source.width : preview.videoWidth; + canvas.height=native ? source.height : preview.videoHeight; + canvas.getContext('2d').drawImage(source,0,0,canvas.width,canvas.height); + const poster=await new Promise(resolve=>canvas.toBlob(resolve,'image/jpeg',0.9)); + if(!poster)throw Error('Video poster failed'); + emit(stream,'poster',{data:poster}); + } catch(error) { + recordError(error);emit(stream,'recorderstatechange',{newState:'MediaRecorderFailed'}); + } finally { + captured.getTracks().forEach(t=>t.stop()); + chunks.length=0;recording=null;finish(); + emit(stream,'recorderstatechange',{newState:'Stopped'}); + } + }; + recording={recorder,done}; + recorder.start(500); + emit(stream,'recorderstatechange',{newState:'Started'}); + } catch(error) { + captured?.getTracks().forEach(t=>t.stop());audio?.getTracks().forEach(t=>t.stop()); + recording=null;throw error; + } finally {startingRecording=false;} + }, + stopRecording() { + if(recording?.recorder.state==='recording')recording.recorder.stop(); + }, + async vulpesFinishRecording() { + if(!recording)return; + const done=recording.done;stream.stopRecording();await done; }, - stopRecording() {}, }); if (native) Object.defineProperty(stream,'zoom',{ configurable:true, get:()=>zoom, @@ -433,7 +568,7 @@ }); dispatchEvent( new CustomEvent('vulpes-camera-capabilities', { - detail: { cameras: [...cameras], flash: false, video: false }, + detail: { cameras: [...cameras], flash: false, video: !!videoMime }, }), ); return { camera: stream, configuration: configuration() }; @@ -479,7 +614,7 @@ 'Only one camera available', 'toggle-camera-rear', ], - ['.mode-switch', true, 'Vidéo pas encore disponible', 'Video is not available yet', null], + ['.mode-switch', !videoMime, 'Vidéo pas encore disponible', 'Video is not available yet', null], ]; const fr = (document.documentElement.lang || navigator.language).startsWith('fr'); for (const [selector, disabled, french, english, icon] of controls) { @@ -489,7 +624,7 @@ e.setAttribute('aria-disabled', String(disabled)); if (!disabled) { e.removeAttribute('title'); - e.setAttribute('aria-label', fr ? 'Changer de caméra' : 'Switch camera'); + if(selector==='.js-camera')e.setAttribute('aria-label', fr ? 'Changer de caméra' : 'Switch camera'); } if (disabled) { e.removeAttribute('data-l10n-id'); diff --git a/host/gaia-compat.js b/host/gaia-compat.js index b0c4627b..a56ecc2c 100644 --- a/host/gaia-compat.js +++ b/host/gaia-compat.js @@ -67,7 +67,25 @@ const unsupported = () => Promise.reject(new DOMException('Not available on this adapter', 'NotSupportedError')); telephony.conferenceGroup = Object.assign(new EventTarget(), {calls:[], state:'', add:unsupported, remove:unsupported, hangUp:unsupported}); telephony.ready = Promise.resolve(); - telephony.dial = number => call('platform.dial',{number}); + class TelephonyCall extends EventTarget {} + Object.defineProperty(window,'TelephonyCall',{value:TelephonyCall}); + function makeCall(item) { + return Object.assign(new TelephonyCall(),{path:item.path,id:{number:item.number},serviceId:0, + state:({active:'connected',waiting:'incoming'})[item.state] || item.state, + emergency:false,group:null,switchable:false,mergeable:false, + answer:()=>call('platform.answer',{path:item.path}),hangUp:()=>call('platform.hangup',{path:item.path}), + hold:unsupported,resume:unsupported}); + } + telephony.dial = async number => { + const result = await call('platform.dial',{number}); + let current = telephony.calls.find(c=>c.path===result.path); + if (!current) { + current = makeCall({path:result.path,number,state:'dialing'}); + telephony.calls.push(current); + emit(telephony,'callschanged'); + } + return current; + }; telephony.dialEmergency = unsupported; telephony.sendTones = unsupported; telephony.startTone = unsupported; @@ -94,14 +112,17 @@ const state = ({active:'connected',waiting:'incoming'})[item.state] || item.state; if (!c) { changed = true; - c = Object.assign(new EventTarget(),{path:item.path,id:{number:item.number}, serviceId:0, - state, emergency:false, group:null, switchable:false, mergeable:false, - answer:()=>call('platform.answer',{path:item.path}), hangUp:()=>call('platform.hangup',{path:item.path}), - hold:unsupported,resume:unsupported}); + c = makeCall(item); } else if (c.state !== state) { c.state=state;changes.push(c); } return c; }); - for (const c of old) if (!telephony.calls.includes(c)) {c.state='disconnected';changes.push(c);changed=true;} + for (const c of old) if (!telephony.calls.includes(c)) { + c.state='disconnected'; + // The broker currently supplies no modem disconnect cause. Avoid + // inventing an error when a call simply disappears after hang-up. + c.disconnectedReason=null; + changes.push(c);changed=true; + } for (const c of changes) {emit(c,'statechange',{call:c});emit(c,c.state,{call:c});} if (changed) { if (location.hostname === 'system.localhost' && live.some(c => ['incoming','waiting'].includes(c.state))) window.Service?.request('turnScreenOn'); @@ -131,13 +152,13 @@ const settingsCodec = window.VulpesSettingsCodec; async function call(method, params) { if (!navigator.vulpesRequest) return Promise.reject(new Error('VULPES_HOST_UNAVAILABLE')); - if (settingsCodec && (method === 'settings.set' || + if (settingsCodec && (method === 'settings.set' || method === 'messages.request' || (window.VulpesPlatform === 'android' && ['media.request', 'activities.finish', 'contacts.request'].includes(method)))) params = await settingsCodec.encode(params); return navigator.vulpesRequest({ version: 1, id: ++sequence, method, params }).then((reply) => { if (reply.error) throw new DOMException(reply.error.code, reply.error.code); return settingsCodec && - ['settings.get', 'settings.snapshot', 'media.request', 'activities.start', 'contacts.request'].includes(method) + ['messages.request', 'settings.get', 'settings.snapshot', 'media.request', 'activities.start', 'contacts.request'].includes(method) ? settingsCodec.decode(reply.result) : reply.result; }); @@ -217,11 +238,15 @@ } if (type === 'callscreen-closed') document.querySelector('iframe[name="call_screen"]')?.dispatchEvent(new CustomEvent('mozbrowserclose',{bubbles:true})); if (type === 'hardware-key') dispatchEvent(new CustomEvent('softwareButtonEvent', {detail:data})); + if (type === 'activity-choice') dispatchEvent(new CustomEvent('mozChromeEvent', {detail:{...data,type:'activity-choice'}})); if (type === 'activity-launch') dispatchEvent(new CustomEvent('open-app', {detail:{...data,showApp:true,onlyShowApp:false}})); if (type === 'launch') dispatchEvent(new CustomEvent('webapps-launch', { detail: data })); if (type === 'screen-off') { - dispatchEvent(new CustomEvent('secure-killapps')); - window.Service?.request('lock').catch(console.error); + settings.createLock().get('lockscreen.enabled').then(values => { + if (values['lockscreen.enabled'] === false) return; + dispatchEvent(new CustomEvent('secure-killapps')); + window.Service?.request('lock').catch(console.error); + }).catch(console.error); } if (type === 'screen-on') window.ScreenManager?.turnScreenOn(true); if (type === 'home' || type === 'holdhome') { @@ -230,6 +255,10 @@ } if (type === 'activity-view') dispatchEvent(new CustomEvent('activity-view', { detail: data })); }); + if (location.hostname==='system.localhost') addEventListener('mozContentEvent', event=>{ + if (event.detail?.type==='activity-choice') + call('activities.select',{id:event.detail.id,value:event.detail.value}).catch(console.error); + }); function application(record) { // DOMApplication never exposed the registry's internal id. Gaia uses `id` // to distinguish a bookmark from an installed application. @@ -520,7 +549,7 @@ frames.add(frame); // Set delegation before navigation; a MutationObserver runs too late on Android. if (location.hostname === 'system.localhost') - frame.setAttribute('allow', 'camera http://camera.localhost:' + location.port); + frame.setAttribute('allow', 'camera http://camera.localhost:' + location.port + '; microphone http://camera.localhost:' + location.port); frame.addEventListener('load', () => { if (!frame.hasAttribute('mozbrowser')) return; if (webViews.has(frame)) return; diff --git a/host/gaia-input.js b/host/gaia-input.js index 662aee9b..59f6c7e3 100644 --- a/host/gaia-input.js +++ b/host/gaia-input.js @@ -1,6 +1,10 @@ // Keep Gaia's keyboard and layouts; replace its retired Gecko input-method API. (() => { - if (!window.VulpesNativeInput) return; + function start() { + // Android emits focus only when enabled. Install the keyboard API before Gaia's + // startup scripts, without waiting for the asynchronous preference handshake. + if ((!window.VulpesNativeInput && window.VulpesPlatform !== 'android') || window.__vulpesInputInstalled) return; + window.__vulpesInputInstalled=true; const app=location.hostname.split('.')[0], {call}=VulpesCompat; if (app==='keyboard') { let viewport, pendingState; @@ -16,13 +20,14 @@ dispatchEvent(new Event('resize')); if(pendingState!==undefined) {setState(pendingState);pendingState=undefined;} }); - addEventListener('DOMContentLoaded', () => { + const ready = () => { ViewManager.prototype.screenInPortraitMode = function() { return viewport ? viewport.width<=viewport.height : this.cachedWindowWidth<=this.cachedWindowHeight; }; parent.postMessage({type:'vulpes-keyboard-ready'},'http://system.localhost:8765'); - }, {once:true}); + }; + if(document.readyState==='loading')addEventListener('DOMContentLoaded',ready,{once:true});else ready(); const ime=Object.assign(new EventTarget(),{inputcontext:null,mgmt:{ supportsSwitching:()=>false, hide:()=>call('input.request',{operation:'hide'}), @@ -93,4 +98,7 @@ for(const type of ['home','lockscreen-appopened','appwillclose']) addEventListener(type,()=>call('input.request',{operation:'hide'}).catch(console.error)); } + } + addEventListener('vulpes-input-configured',start); + start(); })(); diff --git a/host/gaia-mobile-settings.js b/host/gaia-mobile-settings.js new file mode 100644 index 00000000..628e1849 --- /dev/null +++ b/host/gaia-mobile-settings.js @@ -0,0 +1,366 @@ +// Expose the single native SIM to the original Gaia Settings controllers. +// Other applications retain their existing telephony/message adapters. +(() => { + if (location.hostname !== 'settings.localhost' || !window.VulpesNativeTelephony) return; + const {call} = VulpesCompat; + const read = action => call('platform.mobileSettings', {action}); + const unavailable = () => Promise.reject(new DOMException('Not supported by this adapter', 'NotSupportedError')); + const icc = Object.assign(new EventTarget(), {cardState:'unknown', iccInfo:null}); + const conn = Object.assign(new EventTarget(), {iccId:null, radioState:'unknown', supportedNetworkTypes:[], voice:{type:null}, data:{type:null}}); + const manager = Object.assign(new EventTarget(), {iccIds:[], getIccById:id => id && id === conn.iccId ? icc : null}); + let fixedDialing, pinEnabled; + icc.getCardLock = async name => { + await VulpesMobileSettings.ready; + const enabled = name === 'fdn' ? fixedDialing : name === 'pin' ? pinEnabled : undefined; + if (typeof enabled !== 'boolean') return unavailable(); + return {enabled}; + }; + // Fixed-dialling changes, PIN operations and SIM contacts are not ported. + icc.getServiceState = () => Promise.resolve(false); + for (const method of ['unlockCardLock','setCardLock','getCardLockRetryCount','readContacts','updateContact']) icc[method] = unavailable; + // Supplementary services can fail independently of reading the SIM itself. + // Do not pretend a modem query is a successful preference change. + conn.getCallingLineIdRestriction = unavailable; + conn.setCallingLineIdRestriction = unavailable; + conn.getCallWaitingOption = () => read('calls').then(value => { + if (!['enabled','disabled'].includes(value.VoiceCallWaiting)) throw Error('RADIO_NOT_READY'); + return value.VoiceCallWaiting === 'enabled'; + }); + conn.setCallWaitingOption = unavailable; + conn.getVoicePrivacyMode = unavailable; conn.setVoicePrivacyMode = unavailable; + Object.defineProperty(navigator,'mozMobileConnections',{value:[conn]}); + Object.defineProperty(navigator,'mozIccManager',{value:manager}); + Object.defineProperty(navigator,'mozMobileMessage',{value:{getSmscAddress:() => read('smsc').then(v=>({address:v.address || ''}))}}); + async function refresh() { + const state = await read('sim'); + const previous = conn.iccId; + conn.radioState = state.radioEnabled ? 'enabled' : 'disabled'; + conn.dispatchEvent(new Event('radiostatechange')); + conn.iccId = state.present ? state.id : null; + fixedDialing = state.fixedDialing; pinEnabled = state.pinEnabled; + manager.iccIds = conn.iccId ? [conn.iccId] : []; + icc.cardState = !state.present ? 'absent' : state.pin === 'none' ? 'ready' : state.pin === 'pin' ? 'pinRequired' : state.pin === 'puk' ? 'pukRequired' : 'unknown'; + icc.iccInfo = {iccid:conn.iccId, spn:state.operator, msisdn:state.numbers?.[0] || '', iccType:'sim', isDisplaySpnRequired:true}; + if (previous !== conn.iccId) manager.dispatchEvent(Object.assign(new Event(conn.iccId ? 'iccdetected' : 'iccundetected'),{iccId:conn.iccId || previous})); + icc.dispatchEvent(new Event('cardstatechange')); icc.dispatchEvent(new Event('iccinfochange')); + } + let messageState, changing = false; + async function messaging(panel) { + const control=panel.querySelector('[name="ril.sms.requestStatusReport.enabled"]'); + if (!control || control.dataset.vulpesBound) return; + control.dataset.vulpesBound='true'; control.disabled=true; + control.addEventListener('change', async event => { + event.stopImmediatePropagation(); + const desired=control.checked; + if (changing || typeof messageState !== 'boolean') return; + changing=true;control.disabled=true; + try { + const result=await call('platform.mobileSettings',{action:'deliveryReports',enabled:desired}); + messageState=result.deliveryReports; + await navigator.mozSettings.createLock().set({'ril.sms.requestStatusReport.enabled':messageState}); + } catch(error) {window.VulpesGaiaUI?.notice('SMS: '+error.message);} + finally {control.checked=messageState;changing=false;control.disabled=false;} + },true); + try { + const state=await read('messages'); + if (typeof state.deliveryReports !== 'boolean') return; + messageState=state.deliveryReports; + control.checked=messageState; + await navigator.mozSettings.createLock().set({'ril.sms.requestStatusReport.enabled':messageState}); + control.disabled=false;control.closest('li').removeAttribute('aria-disabled'); + control.dataset.vulpesSupported='true'; + } catch(error) {console.warn('Message settings:',error.message);} + } + function carrierPanel() { + let panel, state, pending=false; + const controls = () => [panel.querySelector('#menuItem-enableDataCall gaia-switch'), panel.querySelector('#menuItem-enableDataRoaming gaia-switch')]; + async function sync() { + if(typeof state?.enabled==='boolean' && typeof state?.roaming==='boolean') + await navigator.mozSettings.createLock().set({'ril.data.enabled':state.enabled,'ril.data.roaming_enabled':state.roaming}); + } + function render() { + const [data,roaming]=controls(); + for(const [control,key] of [[data,'enabled'],[roaming,'roaming']]) { + control.disabled=pending || typeof state?.[key]!=='boolean'; + control.checked=state?.[key]===true; + control.closest('li').setAttribute('aria-disabled',String(control.disabled)); + } + panel.querySelector('.carrier-info').hidden=false; + panel.querySelector('.carrier-advancedSettings').hidden=false; + panel.querySelector('#dataNetwork-desc').textContent=[state?.operator,(state?.technology||'').toUpperCase(),state?.status].filter(Boolean).join(' · '); + } + return { + onInit(element) { + panel=element; + for(const [control,action] of [[controls()[0],'dataEnabled'],[controls()[1],'roaming']]) { + control.addEventListener('change',async event=>{ + event.stopImmediatePropagation();if(pending)return; + const enabled=control.checked;pending=true;render(); + try {state=await call('platform.mobileSettings',{action,enabled});await sync();} + catch(error) {window.VulpesGaiaUI?.notice(error.message);} + finally {pending=false;render();} + },true); + } + }, + async onBeforeShow() { + pending=true;render(); + try {state=await read('network');await sync();} + catch(error) {window.VulpesGaiaUI?.notice(error.message);} + finally {pending=false;render();} + } + }; + } + const fr = () => document.documentElement.lang.toLowerCase().startsWith('fr'); + const text = (f,e) => fr()?f:e; + const navigate = (id,options={}) => require(['modules/settings_service'],service=>service.navigate(id,options)); + function explain(panel,key,message) { + let row=panel.querySelector('[data-vulpes-note="'+key+'"]'); + if(!row) {row=document.createElement('p');row.className='explanation';row.dataset.vulpesNote=key;(panel.querySelector(':scope > div')||panel).prepend(row);} + row.textContent=message; + } + function failure(error) { + const messages={ + APN_IN_USE:text('Désactivez les données mobiles avant de modifier ce profil actif.','Turn off mobile data before editing this active profile.'), + APN_WRITE_FAILED:text('Enregistrement impossible. Le profil précédent a été restauré.','Could not save. The previous profile was restored.'), + APN_ROLLBACK_FAILED:text('Le profil n’a pas pu être restauré. Vérifiez ses paramètres avant de réactiver les données.','The profile could not be restored. Check its settings before re-enabling data.'), + RADIO_READBACK_FAILED:text('Le réseau n’a pas confirmé le réglage. Son état va être relu.','The network did not confirm the setting. Its state will be read again.'), + INVALID_REQUEST:text('Vérifiez les valeurs saisies.','Check the values you entered.') + }; + window.VulpesGaiaUI?.notice(messages[error.message]||text('Le service réseau n’a pas pu appliquer ce réglage.','The network service could not apply this setting.')); + } + function callPanel() { + let panel,waiting,caller,state,pending=false; + const modes={default:'CLIR_DEFAULT',enabled:'CLIR_INVOCATION',disabled:'CLIR_SUPPRESSION'}; + function render() { + waiting.disabled=pending || !['enabled','disabled'].includes(state?.VoiceCallWaiting); + waiting.checked=state?.VoiceCallWaiting==='enabled'; + waiting.closest('li').dataset.state=waiting.disabled?'unknown':waiting.checked?'on':'off'; + caller.disabled=pending || !Object.hasOwn(modes,state?.HideCallerId); + caller.value=modes[state?.HideCallerId]||''; + for(const c of [waiting,caller])c.closest('li').setAttribute('aria-disabled',String(c.disabled)); + explain(panel,'call-state',pending?text('Interrogation du réseau… Cela peut prendre plusieurs secondes.','Querying the network… This may take several seconds.'):text('Le double appel et la présentation du numéro utilisent les réglages de votre opérateur.','Call waiting and caller ID use your carrier’s settings.')); + } + async function update(params) { + if(pending)return; + pending=true;render(); + try {state=await call('platform.mobileSettings',params);} + catch(error) {state=null;failure(error);try {state=await read('calls');}catch(_) {}} + finally {pending=false;render();} + } + return {onInit(element){ + panel=element;waiting=panel.querySelector('#menuItem-callWaiting gaia-switch');caller=panel.querySelector('#ril-callerId');caller.removeAttribute('data-setting'); + for(const id of ['menuItem-callWaiting','menuItem-callerId','menuItem-callForwarding','menuItem-callFdn','menuItem-callBarring'])panel.querySelector('#'+id).hidden=false; + waiting.addEventListener('change',()=>update({action:'setCallWaiting',enabled:waiting.checked})); + caller.addEventListener('change',()=>update({action:'setCallerId',mode:Object.keys(modes).find(key=>modes[key]===caller.value)})); + for(const [id,target] of [['menuItem-callForwarding','call-forwarding'],['menuItem-callFdn','call-fdnSettings'],['menuItem-callBarring','call-cbSettings']]) { + const a=panel.querySelector('#'+id+' a');a.removeAttribute('data-href');a.href='#'+target; + a.addEventListener('click',event=>{event.preventDefault();event.stopImmediatePropagation();navigate(target);},true); + } + const voicemail=panel.querySelector('.menuItem-voicemail');voicemail.setAttribute('aria-disabled','true'); + voicemail.addEventListener('click',event=>{event.preventDefault();event.stopImmediatePropagation();},true); + document.l10n.setAttributes(panel.querySelector('#voiceMail-desc'),'unavailable'); + explain(panel,'call-limits',text('Le numéro de messagerie vocale, les numéros autorisés et le blocage des appels ne sont pas encore raccordés.','Voicemail number, fixed dialling and call barring are not connected yet.')); + render(); + },onBeforeShow(){void update({action:'calls'});}}; + } + function forwardingPanel() { + let panel,state,pending=false; + const rows={ + unconditional:['cfu','call-cf-unconditional-settings','.callForwardingUnconditional'], + busy:['cfmb','call-cf-mobile-busy-settings','.callForwardingMobileBusy'], + noReply:['cfnrep','call-cf-no-reply-settings','.callForwardingNoReply'], + notReachable:['cfnrea','call-cf-not-reachable-settings','.callForwardingNotReachable'] + }; + function render() { + for(const [reason,[id,,selector]] of Object.entries(rows)) { + const known=typeof state?.[reason]==='string'; + panel.querySelector(selector).setAttribute('aria-disabled',String(pending||!known)); + const node=panel.querySelector('#'+id+'-desc'); + document.l10n.setAttributes(node,pending?'callSettingsQuery':!known?'callSettingsQueryError':state[reason]?'callForwardingForwardingVoiceToNumber':'callForwardingNotForwarding',state?.[reason]?{number:state[reason]}:{}); + } + } + async function load() { + if(pending)return; + pending=true;render(); + try {state=await read('forwarding');}catch(error){state=null;failure(error);} + finally {pending=false;render();} + } + return {onInit(element){ + panel=element; + for(const [reason,[,dialog,selector]] of Object.entries(rows))panel.querySelector(selector).addEventListener('click',async event=>{ + event.preventDefault();event.stopImmediatePropagation(); + if(pending || typeof state?.[reason]!=='string')return; + const service=await new Promise(resolve=>require(['modules/dialog_service'],resolve)); + const result=await service.show(dialog,{number:state[reason],enabled:!!state[reason]}); + if(result.type!=='submit')return; + const enabled=!!result.value.enabled; + const number=enabled?String(result.value.number||'').replace(/[\s()-]/g,''):''; + pending=true;render(); + try {state=await call('platform.mobileSettings',{action:'setForwarding',reason,enabled,number});} + catch(error){failure(error);try{state=await read('forwarding');}catch(_){state=null;}} + finally {pending=false;render();} + }); + },onBeforeShow(){void load();}}; + } + + function operatorPanel() { + let panel,select,pending=false,state; + return { + onInit(element) { + panel=element;select=panel.querySelector('.preferred-network-type select'); + // Operator scanning/registration has a different service and remains + // unavailable; it must not be confused with the radio mode selector. + panel.querySelector('.auto-select gaia-switch').disabled=true; + panel.querySelector('.search-btn').disabled=true; + select.addEventListener('change',async()=>{ + if(pending || !state)return; + const mode=select.value;pending=true;select.disabled=true; + try {state=await call('platform.mobileSettings',{action:'setRadioMode',mode});} + catch(error){failure(error);} + finally {select.value=state.mode;pending=false;select.disabled=false;} + }); + }, + async onBeforeShow() { + select.disabled=true; + explain(panel,'operator',text('Le type de réseau est réglable. La recherche et la sélection manuelle d’opérateur ne sont pas encore raccordées.','Network type can be changed. Operator scanning and manual selection are not connected yet.')); + try { + const [radio,network]=await Promise.all([read('radioMode'),read('network')]);state=radio; + select.replaceChildren(); + const names={any:text('Automatique','Automatic'),gsm:'2G (GSM)',umts:'3G (UMTS)',lte:'4G (LTE)'}; + for(const mode of radio.available)select.add(new Option(names[mode]||mode,mode)); + select.value=radio.mode;select.disabled=!radio.available.includes(radio.mode); + panel.querySelector('.auto-select gaia-switch').checked=network.mode==='auto'; + } catch(error){failure(error);} + } + }; + } + function apnSettingsPanel() { + return {onInit(panel){ + for(const anchor of panel.querySelectorAll('[data-apn-type]')) { + const supported=['default','mms'].includes(anchor.dataset.apnType); + if(!supported)anchor.setAttribute('aria-disabled','true'); + anchor.addEventListener('click',event=>{ + event.preventDefault();event.stopImmediatePropagation(); + if(supported)navigate('apn-list',{type:anchor.dataset.apnType}); + }); + } + panel.querySelector('.reset-apn').disabled=true; + },onBeforeShow(panel){ + explain(panel,'apn',text('Modifiez les profils Internet et MMS existants. Les profils IMS restent gérés par le modem. L’ajout, la suppression et la réinitialisation ne sont pas encore disponibles.','Edit existing Internet and MMS profiles. IMS profiles remain managed by the modem. Adding, deleting and resetting profiles are not available yet.')); + }}; + } + function apnListPanel() { + let panel,type='default'; + return {onInit(element){panel=element;panel.querySelector('.add-apn').disabled=true;panel.querySelector('gaia-header').dataset.href='#apn-settings';}, + async onBeforeShow(element,options={}) { + type=options.type==='mms'?'mms':'default'; + document.l10n.setAttributes(panel.querySelector('h1'),type==='mms'?'messageSettings-header':'dataSettings-header'); + const list=panel.querySelector('.apn-list');list.replaceChildren(); + try { + const profiles=(await read('apns')).filter(p=>p.type===type); + const template=await new Promise(resolve=>require(['panels/apn_list/apn_template_factory'],factory=>resolve(factory(type,item=>navigate('apn-editor',{profileId:item.id,type}),()=>{})))); + for(const profile of profiles) { + const li=template({id:profile.id,apn:{apn:profile.fields.apn},active:profile.active}); + // This mark reflects an active data context; changing the profile + // does not activate/deactivate a bearer implicitly. + const radio=li.querySelector('gaia-radio');radio.setAttribute('aria-readonly','true'); + list.append(li); + } + explain(panel,'list',profiles.length?text('Touchez le nom du profil pour le modifier.','Tap a profile name to edit it.'):text('Aucun profil fourni par le modem.','The modem returned no profile.')); + }catch(error){failure(error);} + } + }; + } + function apnEditorPanel() { + let panel,profile,pending=false,dirty=new Set(); + const selectors={apn:'.apn',user:'.user',password:'.password',protocol:'.protocol',authtype:'.authtype',mmsc:'.mmsc',mmsproxy:'.mmsproxy'}; + const protocols={ip:'IP',ipv6:'IPV6',dual:'IPV4V6'}; + const auth={none:'none',pap:'pap',chap:'chap',any:'papOrChap'}; + function editable() { + for(const [key,selector] of Object.entries(selectors))panel.querySelector(selector).disabled=pending || !profile || (['mmsc','mmsproxy'].includes(key)&&profile.type!=='mms'); + panel.querySelector('.ok').disabled=pending || !profile; + } + function render() { + editable(); + for(const [key,selector] of Object.entries(selectors)) { + const input=panel.querySelector(selector); + input.disabled=pending || !profile || (['mmsc','mmsproxy'].includes(key)&&profile.type!=='mms'); + const value=profile?.fields[key]||''; + input.value=key==='password'?'':key==='protocol'?protocols[value]||'notDefined':key==='authtype'?auth[value]||'notDefined':value; + if(key==='password')input.placeholder=profile?.hasPassword?text('Inchangé','Unchanged'):''; + } + panel.querySelector('.ok').disabled=pending || !profile; + } + return {onInit(element){ + panel=element; + for(const input of panel.querySelectorAll('input,select,button'))input.disabled=true; + for(const [key,selector] of Object.entries(selectors))for(const event of ['input','change'])panel.querySelector(selector).addEventListener(event,()=>dirty.add(key)); + panel.querySelector('.ok').addEventListener('click',async()=>{ + if(pending||!profile)return; + const fields={}; + for(const key of dirty) { + const value=panel.querySelector(selectors[key]).value; + fields[key]=key==='protocol'?Object.keys(protocols).find(k=>protocols[k]===value):key==='authtype'?Object.keys(auth).find(k=>auth[k]===value):value; + if(fields[key]===undefined){failure({message:'INVALID_REQUEST'});return;} + } + if(!dirty.size){navigate('apn-list',{type:profile.type});return;} + pending=true;editable(); + try { + profile=await call('platform.mobileSettings',{action:'editApn',id:profile.id,fields}); + dirty.clear();navigate('apn-list',{type:profile.type}); + }catch(error){failure(error);} + finally {pending=false;editable();} + }); + panel.querySelector('gaia-header').addEventListener('action',event=>{event.stopImmediatePropagation();if(!pending)navigate('apn-list',{type:profile?.type||'default'});},true); + },async onBeforeShow(element,options={}) { + profile=null;dirty.clear();render(); + explain(panel,'fields',text('Les champs grisés ne sont pas gérés par ce modem. Le champ mot de passe vide conserve le secret existant tant que vous ne le modifiez pas.','Greyed-out fields are not handled by this modem. An untouched password field keeps the existing secret.')); + try {profile=(await read('apns')).find(p=>p.id===options.profileId);if(!profile)throw Error('APN_NOT_FOUND');render();} + catch(error){failure(error);} + },onHide(){panel.querySelector('.password').value='';dirty.clear();}}; + } + + let clockState, clockUpdating=false; + Object.defineProperty(navigator,'mozTime',{value:{set:async date=>{ + try { + await call('platform.deviceSettings',{action:'setTime',milliseconds:+date}); + dispatchEvent(new Event('moztimechange')); + } catch(error) {window.VulpesGaiaUI?.notice(error.message);} + }}}); + async function clock() { + clockState=await call('platform.deviceSettings',{action:'clock'}); + clockUpdating=true; + try {await navigator.mozSettings.createLock().set({ + 'time.timezone':clockState.timezone, 'time.timezone.user-selected':clockState.timezone, + 'time.clock.automatic-update.available':clockState.automaticAvailable, + 'time.clock.automatic-update.enabled':clockState.automatic, + 'time.timezone.automatic-update.available':false + });} finally {clockUpdating=false;} + return clockState; + } + navigator.mozSettings.addObserver('time.clock.automatic-update.enabled',async event=>{ + if(clockUpdating || !clockState || event.settingValue===clockState.automatic)return; + try { + clockState=await call('platform.deviceSettings',{action:'setAutomatic',enabled:event.settingValue}); + await call('settings.clockChanged',{}); + dispatchEvent(new Event('moztimechange')); + } catch(error) { + window.VulpesGaiaUI?.notice(error.message); + await clock(); + } + }); + navigator.mozSettings.addObserver('time.timezone',async event=>{ + if(clockUpdating || !clockState || event.settingValue===clockState.timezone)return; + try { + clockState=await call('platform.deviceSettings',{action:'setTimezone',timezone:event.settingValue}); + await call('settings.clockChanged',{}); + dispatchEvent(new Event('moztimechange')); + } catch(error) { + window.VulpesGaiaUI?.notice(error.message); + await navigator.mozSettings.createLock().set({'time.timezone':clockState.timezone}); + } + }); + clock().catch(error=>console.warn('Clock settings:',error.message)); + window.VulpesMobileSettings = {ready:refresh(), refresh, messaging, clock, panels:{call:callPanel,'call-forwarding':forwardingPanel,carrier:carrierPanel,'operator-settings':operatorPanel,'apn-settings':apnSettingsPanel,'apn-list':apnListPanel,'apn-editor':apnEditorPanel}}; + VulpesMobileSettings.ready.catch(error=>console.warn('SIM settings:',error.message)); +})(); diff --git a/host/gaia-restoration.js b/host/gaia-restoration.js index d5d72a6f..1ac4bb39 100644 --- a/host/gaia-restoration.js +++ b/host/gaia-restoration.js @@ -3,6 +3,22 @@ const app = location.hostname.split('.')[0]; const fr = () => (document.documentElement.lang || navigator.language).startsWith('fr'); const label = (a, b) => fr() ? a : b; + const labels = new Map(); + function localize(node, french, english, attribute) { + labels.set(node, {french, english, attribute}); + translate(); + } + function translate() { + for (const [node, entry] of labels) { + const value = label(entry.french, entry.english); + if (entry.attribute) { + if (node.getAttribute(entry.attribute) !== value) node.setAttribute(entry.attribute, value); + } else if (node.textContent !== value) node.textContent = value; + } + } + addEventListener('localized', translate); + new MutationObserver(translate).observe(document.documentElement, {attributes:true, attributeFilter:['lang']}); + function notice(message) { document.getElementById('vulpes-gaia-notice')?.remove(); const link = document.createElement('link'); @@ -28,6 +44,14 @@ window.VulpesGaiaUI = {notice}; async function ready() { if (app === 'system') { + // Android reserves the edge swipe for its own shade. A tap on Gaia's + // status bar provides access without competing with that system gesture. + if (window.VulpesPlatform === 'android') { + document.getElementById('statusbar')?.addEventListener('click', () => { + if (!window.Service?.query('locked')) window.UtilityTray?.show(true); + }); + } + // OperatorIcon also owns the date. It must exist even without a modem. const startDate = async () => { if (!window.BaseIcon || !window.Service || !window.LazyLoader) return false; @@ -50,8 +74,8 @@ const li = document.createElement('li'), button = document.createElement('button'), error = document.createElement('p'); li.id = 'import-sim-option-unavailable';li.setAttribute('role','presentation'); button.className = 'icon icon-sim';button.setAttribute('role','option');button.disabled = true; - button.textContent = label('Carte SIM', 'SIM card'); - error.className = 'error-message';error.textContent = unavailable(); + localize(button, 'Carte SIM', 'SIM card'); + error.className = 'error-message';localize(error, 'Cette fonction n’est pas encore disponible sur cette plateforme.', 'This feature is not available on this platform yet.'); li.append(button,error);list.prepend(li); } const item = document.getElementById('import-sd-option'); @@ -62,7 +86,7 @@ if(item.classList.contains('disabled')) item.classList.remove('disabled'); const span = item.querySelector('span'); if(span && span.dataset.l10nId === 'importMemoryCard') { - span.removeAttribute('data-l10n-id');span.textContent = label('Fichier de contacts', 'Contact file'); + span.removeAttribute('data-l10n-id');localize(span, 'Fichier de contacts', 'Contact file'); } const error = item.querySelector('p'); if(error && !error.hidden) error.hidden = true; @@ -79,6 +103,17 @@ }, true); } + if (app === 'sms') { + let mmsAvailable=false; + VulpesCompat.call('platform.capabilities',{}).then(c=>{mmsAvailable=c.mms===true;}).catch(()=>{}); + // Keep the composer intact until a real MMS transport is available. + document.addEventListener('click', event => { + if (mmsAvailable || !event.target.closest('#messages-send-button') || window.Compose?.type !== 'mms') return; + event.preventDefault(); event.stopImmediatePropagation(); + notice(label('L’envoi de MMS n’est pas encore disponible dans cette version. Votre message et ses pièces jointes sont conservés.', + 'MMS sending is not yet available in this version. Your message and attachments have been kept.')); + }, true); + } if (app === 'sms' && window.VulpesPlatform === 'android') { document.addEventListener('click', event => { if (!event.target.closest('#messages-send-button')) return; @@ -103,7 +138,7 @@ if (!header || header.querySelector('[data-vulpes-import]')) return; const action = document.createElement('button'); action.dataset.vulpesImport = ''; action.dataset.icon = 'add'; - action.setAttribute('aria-label', label('Importer des fichiers', 'Import files')); + localize(action, 'Importer des fichiers', 'Import files', 'aria-label'); action.onclick = () => VulpesCompat.call('platform.pick',{type:mediaType}).catch(error => { if (error.name !== 'AbortError' && error.message !== 'AbortError') notice(unavailable()); }); @@ -134,31 +169,79 @@ const unsupported = { wifi: !navigator.mozWifiManager, bluetooth: !navigator.mozBluetooth, - carrier: !navigator.mozMobileConnections, - call: !navigator.mozMobileConnections, - messaging: !navigator.mozMobileConnections, + carrier: !window.VulpesMobileSettings, + call: !window.VulpesMobileSettings, + messaging: !navigator.mozMobileConnections?.length, 'sim-manager': !navigator.mozIccManager, - simpin: !navigator.mozIccManager, - hotspot: !navigator.mozWifiManager, + simpin: true, + 'wifi-manageCertificates': true, + 'wifi-selectCertificateFile': true, + hotspot: !window.VulpesWifiSettings, + downloads: capabilities.platform === 'android', + 'wifi-manageNetworks': !navigator.mozWifiManager, + 'messaging-details': !navigator.mozMobileConnections?.length, + 'operator-settings': !window.VulpesMobileSettings, + 'carrier-detail': !window.VulpesMobileSettings, + 'apn-settings': !window.VulpesMobileSettings, + 'apn-list': !window.VulpesMobileSettings, + 'apn-editor': !window.VulpesMobileSettings, + 'call-forwarding': !window.VulpesMobileSettings, + 'call-cbSettings': true, + 'call-fdnSettings': true, + 'call-fdnList': true, + 'developer-service-workers': true, + 'developer-hud': true, + 'accessibility-screenreader': true, + crashReports: true, + improveBrowserOS: true, + 'improveBrowserOS-sendFeedback': true, + 'improveBrowserOS-chooseFeedback': true, + 'full-developer-mode': true, findmydevice: true, fxa: true, fxsync: true, addons: true, mediaStorage: !navigator.getDeviceStorages, applicationStorage: !navigator.getDeviceStorage, - usbStorage: !navigator.getDeviceStorages, + usbStorage: true, }; function preservePanel(panel) { - const content = panel.querySelector(':scope > div'); - if (!content || content.querySelector('[data-vulpes-unavailable]')) return; + const content = panel.querySelector(':scope > div') || panel; + if (content.querySelector('[data-vulpes-unavailable]')) return; const explanation = document.createElement('p'); explanation.className = 'explanation'; explanation.dataset.vulpesUnavailable = ''; - explanation.setAttribute('role', 'status'); explanation.textContent = unavailable(); - content.prepend(explanation); + explanation.setAttribute('role', 'status'); localize(explanation, 'Cette fonction n’est pas encore disponible sur cette plateforme.', 'This feature is not available on this platform yet.'); + const header=content===panel ? panel.querySelector(':scope > gaia-header') : null; + if(header)header.after(explanation);else content.prepend(explanation); + const serviceNotes = { + fxa: ['La connexion intégrée au compte Mozilla doit être portée vers l’authentification actuelle. Ouvrir le site du compte ne connecte pas encore Vulpes.', 'Integrated Mozilla account sign-in needs the current authentication protocol. Opening the account website does not sign Vulpes in.'], + fxsync: ['La synchronisation nécessite la connexion au compte Mozilla et un adaptateur pour les données Vulpes. Elle n’est pas encore raccordée.', 'Sync requires Mozilla account sign-in and an adapter for Vulpes data. It is not connected yet.'], + findmydevice: ['La localisation distante nécessite un service Vulpes sécurisé et associé à votre compte. Aucun appareil n’est actuellement suivi.', 'Remote device location requires a secure Vulpes service linked to your account. No device is currently tracked.'], + addons: ['Ce panneau gérait les extensions des applications Firefox OS, pas les extensions du navigateur Firefox. Leur moteur d’exécution reste à porter.', 'This panel managed Firefox OS app add-ons, not Firefox browser extensions. Their runtime still needs to be ported.'], + usbStorage: ['Le transfert de fichiers USB n’est pas encore raccordé. La connexion USB de maintenance reste disponible.', 'USB file transfer is not connected yet. The maintenance USB connection remains available.'] + }; + if(serviceNotes[panel.id]) localize(explanation,...serviceNotes[panel.id]); + if (panel.id === 'wifi-manageCertificates' || panel.id === 'wifi-selectCertificateFile') localize(explanation, 'Les certificats Wi-Fi et les réseaux WPA-Enterprise ne sont pas encore pris en charge par cet adaptateur.', 'Wi-Fi certificates and WPA-Enterprise networks are not supported by this adapter yet.'); + if (panel.id === 'call' && window.VulpesNativeTelephony) { + localize(explanation, 'Les appels fonctionnent. Le renvoi, le double appel et le masquage du numéro ne sont pas encore configurables ici.', 'Calling is available. Call forwarding, call waiting and caller ID cannot be configured here yet.'); + for (const item of content.querySelectorAll('li[hidden]')) item.hidden = false; + } + if(panel.id==='improveBrowserOS') { + const list=content.querySelector('ul'); + for(const child of [...content.children]) if(child!==list && child!==explanation)child.hidden=true; + const anchor=list.querySelector('a');anchor.href='https://nnsprod.com/git/oversu/VulpesOS/issues'; + const title=anchor.querySelector('span');title.removeAttribute('data-l10n-id'); + localize(title,'Signaler un problème ou proposer une idée','Report a problem or suggest an idea'); + localize(explanation,'Les retours sont recueillis sur le dépôt Vulpes OS. Aucun rapport n’est envoyé automatiquement.','Feedback is collected on the Vulpes OS repository. No report is sent automatically.'); + anchor.addEventListener('click',event=>{event.preventDefault();new MozActivity({name:'view',data:{type:'url',url:anchor.href}});}); + return; + } for (const control of content.querySelectorAll('button,input,select,gaia-switch,gaia-checkbox,gaia-radio')) { control.disabled = true; control.setAttribute('disabled', ''); } for (const anchor of content.querySelectorAll('a')) { + const href=anchor.getAttribute('href'); + if(href && href!=='#' && href.startsWith('#') && unsupported[href.slice(1)]) continue; anchor.setAttribute('aria-disabled', 'true'); anchor.addEventListener('click', event => {event.preventDefault();event.stopImmediatePropagation();}, true); } @@ -168,9 +251,19 @@ const install = setInterval(() => { if (typeof window.require !== 'function' || !require.defined?.('modules/settings_panel') || !require.defined?.('modules/panel_cache')) return; clearInterval(install); - require(['modules/panel_cache', 'modules/settings_panel'], (cache, SettingsPanel) => { + require(['modules/panel_cache', 'modules/settings_panel', 'modules/dialog_panel'], (cache, SettingsPanel, DialogPanel) => { const original = cache.get.bind(cache), panels = new Map(); cache.get = (id, callback) => { + const dialog = window.VulpesWifiSettings?.dialogs[id]; + const native = window.VulpesMobileSettings?.panels[id] || window.VulpesWifiSettings?.panels[id]; + if(dialog) { + if(!panels.has(id))panels.set(id,DialogPanel(dialog())); + callback?.(panels.get(id));return; + } + if(native) { + if(!panels.has(id))panels.set(id,SettingsPanel(native())); + callback?.(panels.get(id));return; + } if (!unsupported[id]) return original(id, callback); if (!panels.has(id)) panels.set(id, SettingsPanel({onInit:preservePanel, onBeforeShow:preservePanel})); callback?.(panels.get(id)); @@ -179,26 +272,70 @@ }, 100); function restore() { batteryText(); + if (window.VulpesMobileSettings) { + const panel = document.querySelector('#messaging > div'); + if (panel && !panel.querySelector('[data-vulpes-message-limit]')) { + const p = document.createElement('p'); p.className = 'explanation'; p.dataset.vulpesMessageLimit = ''; + localize(p, 'Les accusés de réception SMS sont reliés au modem. Les autres options grisées ne sont pas encore prises en charge.', 'SMS delivery reports are connected to the modem. The other greyed-out options are not supported yet.'); panel.prepend(p); + } + if (panel) VulpesMobileSettings.messaging(panel); + for (const control of panel?.querySelectorAll('gaia-switch,select') || []) { + if (control.dataset.vulpesSupported === 'true') continue; + if (!control.hasAttribute('disabled')) control.setAttribute('disabled',''); + if (control.disabled !== true) control.disabled = true; + } + } + const clockPanel=document.getElementById('dateTime'); + if(clockPanel?.querySelector('.date-picker') && !navigator.mozTime) { + for(const control of clockPanel.querySelectorAll('.date-picker,.time-picker,.timezone-picker select')) if(!control.disabled)control.disabled=true; + if(!clockPanel.querySelector('[data-vulpes-clock-host]')) { + const row=document.createElement('p');row.className='explanation';row.dataset.vulpesClockHost=''; + localize(row,'La date et le fuseau horaire sont gérés par le système hôte. Le format de l’heure reste réglable ici.', 'Date and timezone are managed by the host system. The time format can still be changed here.'); + clockPanel.querySelector(':scope > div').prepend(row); + } + } + for(const id of ['mediaStorage','applicationStorage']) { + const content=document.querySelector('#'+id+' > div'); + if(content && !content.querySelector('[data-vulpes-storage-scope]')) { + const p=document.createElement('p');p.className='explanation';p.dataset.vulpesStorageScope=''; + localize(p,'Espace utilisé par Vulpes et espace libre sur le volume hôte. Les données des autres applications du système ne sont pas incluses.', 'Vulpes storage usage and free space on the host volume. Other host applications are not included.'); + content.prepend(p); + } + } const root = document.querySelector('#root'); if (root) for (const id of ['wifi','bluetooth','carrier','call','messaging','sim-manager','simpin', 'findmydevice','addons','mediaStorage','applicationStorage']) { const li = root.querySelector('a[href="#' + id + '"]')?.closest('li'); if (li?.hidden) li.hidden = false; + if (li?.hasAttribute('aria-disabled')) li.removeAttribute('aria-disabled'); + const anchor = li?.querySelector('a'); + if (anchor?.hasAttribute('aria-disabled')) anchor.removeAttribute('aria-disabled'); } if (capabilities.platform === 'android' && root && !document.getElementById('vulpes-android-settings')) { const ul = document.createElement('ul'), li = document.createElement('li'), a = document.createElement('a'); a.id = 'vulpes-android-settings'; a.className = 'menu-item'; a.href = '#'; - a.textContent = label('Paramètres Android', 'Android settings'); + localize(a, 'Paramètres Android', 'Android settings'); a.onclick = event => {event.preventDefault();VulpesCompat.call('platform.settings',{panel:'root'}).catch(() => notice(unavailable()));}; li.append(a); ul.append(li); root.querySelector(':scope > div')?.append(ul); } + if (capabilities.platform === 'android') { + const keyboard=document.querySelector('#keyboard > div'); + if(keyboard && !document.getElementById('vulpes-keyboard-mode')) { + const ul=document.createElement('ul'),li=document.createElement('li'),control=document.createElement('gaia-switch'),title=document.createElement('label'); + control.id='vulpes-keyboard-mode'; + localize(title,'Clavier Gaia (expérimental)','Gaia keyboard (experimental)'); + control.append(title);li.append(control);ul.append(li);keyboard.prepend(ul); + VulpesCompat.call('input.config',{}).then(enabled=>{control.checked=enabled;}); + control.addEventListener('change',()=>navigator.mozSettings.createLock().set({'keyboard.gaia.enabled':control.checked}).catch(console.error)); + } + } if (capabilities.platform === 'android') { const homescreens = document.querySelector('#homescreens > div'); if (homescreens && !document.getElementById('vulpes-default-home')) { const ul = document.createElement('ul'), li = document.createElement('li'), a = document.createElement('a'); a.id = 'vulpes-default-home'; a.className = 'menu-item'; a.href = '#'; - a.textContent = label('Accueil Android par défaut', 'Default Android home'); + localize(a, 'Accueil Android par défaut', 'Default Android home'); a.onclick = event => {event.preventDefault();VulpesCompat.call('platform.settings',{panel:'home'}).catch(() => notice(unavailable()));}; li.append(a);ul.append(li);homescreens.append(ul); } @@ -213,12 +350,12 @@ const legal = document.querySelector('#about-legal > div'); if (legal && !document.getElementById('vulpes-credits')) { const p = document.createElement('p'); p.id = 'vulpes-credits'; p.className = 'explanation'; - p.textContent = label('Vulpes OS repose sur Gaia et Gecko de Mozilla et le travail de leurs contributeurs. Capyloon fait partie des projets qui inspirent cette continuité. Les licences et attributions d’origine sont conservées.', + localize(p, 'Vulpes OS repose sur Gaia et Gecko de Mozilla et le travail de leurs contributeurs. Capyloon fait partie des projets qui inspirent cette continuité. Les licences et attributions d’origine sont conservées.', 'Vulpes OS builds on Mozilla Gaia and Gecko and the work of their contributors. Capyloon is among the projects inspiring this continuation. Original licenses and attributions are retained.'); legal.append(p); } } - new MutationObserver(restore).observe(document.body, {subtree:true, childList:true, attributes:true, attributeFilter:['hidden']}); + new MutationObserver(restore).observe(document.body, {subtree:true, childList:true, attributes:true, attributeFilter:['hidden','aria-disabled','disabled']}); restore(); } if (document.readyState === 'loading') addEventListener('DOMContentLoaded', () => ready().catch(console.error), {once:true}); diff --git a/host/gaia-select.js b/host/gaia-select.js new file mode 100644 index 00000000..c6b94c76 --- /dev/null +++ b/host/gaia-select.js @@ -0,0 +1,206 @@ +// Gaia's value-selector markup and stylesheet, without a browser-chrome popup. +(() => { + let active; + function close(commit) { + if (!active) return; + const {select, wrapper, chosen} = active; + active = null; + if (commit && select.isConnected && !select.disabled) { + const before = [...select.options].map(o => o.selected).join(','); + [...select.options].forEach((o, i) => { o.selected = chosen.has(i); }); + if (before !== [...select.options].map(o => o.selected).join(',')) { + select.dispatchEvent(new Event('input', {bubbles:true})); + select.dispatchEvent(new Event('change', {bubbles:true})); + } + } + wrapper.remove(); + // Several original panels commit modem settings on blur. + select.blur(); + } + function open(select) { + if (select.disabled || select.closest('[aria-disabled="true"]')) return; + close(false); + if (!document.querySelector('[data-vulpes-select-style]')) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; link.dataset.vulpesSelectStyle = ''; + link.href = `${location.protocol}//system.localhost:${location.port}/shared/style/value_selector.css`; + document.head.append(link); + } + const wrapper = document.createElement('div'); wrapper.className = 'value-selector'; + wrapper.id = 'vulpes-value-selector-layer'; + const form = document.createElement('form'); + form.id = 'vulpes-value-selector'; form.role = 'dialog'; + form.dataset.type = 'value-selector'; form.setAttribute('aria-modal', 'true'); + form.innerHTML = '

    '; + form.setAttribute('aria-labelledby', 'vulpes-select-title'); + const fr = (document.documentElement.lang || navigator.language).startsWith('fr'); + form.querySelector('[data-action="cancel"]').textContent = fr ? 'Annuler' : 'Cancel'; + form.querySelector('h1').textContent = document.getElementById(select.getAttribute('aria-labelledby'))?.textContent || select.closest('li')?.querySelector('span[data-l10n-id],label,p')?.textContent || (fr ? 'Choisir une option' : 'Choose an option'); + const chosen = new Set([...select.options].flatMap((o,i) => o.selected ? [i] : [])); + const list = form.querySelector('ol'); + if (select.multiple) list.setAttribute('aria-multiselectable', 'true'); + [...select.children].forEach(child => { + if (child.tagName === 'OPTGROUP') { + const heading = document.createElement('li'); heading.role = 'subheader'; + heading.textContent = child.label; list.append(heading); + } + for (const option of child.tagName === 'OPTGROUP' ? child.children : [child]) { + const i = [...select.options].indexOf(option); + if (i < 0 || option.hidden) continue; + const li = document.createElement('li'); li.role = 'option'; li.tabIndex = 0; + li.dataset.index = i; li.dataset.value = option.value; + li.setAttribute('aria-selected', String(chosen.has(i))); + const disabled = option.disabled || option.parentElement.disabled === true; + li.setAttribute('aria-disabled', String(disabled)); + const label = document.createElement('label'), span = document.createElement('span'); + label.role = 'presentation'; span.textContent = option.textContent; + label.append(span); li.append(label); list.append(li); + const pick = () => { + if (disabled) return; + if (!select.multiple) chosen.clear(); + if (select.multiple && chosen.has(i)) chosen.delete(i); else chosen.add(i); + list.querySelectorAll('[role="option"]').forEach(n => n.setAttribute('aria-selected', String(chosen.has(Number(n.dataset.index))))); + }; + li.onclick = pick; + li.onkeydown = event => { if (['Enter',' '].includes(event.key)) {event.preventDefault();pick();} }; + } + }); + active = {select, form, wrapper, chosen}; + select.focus({preventScroll:true}); + form.onsubmit = event => {event.preventDefault();close(true);}; + form.querySelector('[data-action="cancel"]').onclick = () => close(false); + wrapper.append(form); document.body.append(wrapper); + } + // Gecko's embedded native select may consume the compatibility click. + // Open on a completed tap too, without treating a scroll or a hold as a tap. + let gesture, recentTap; + addEventListener('touchstart', event => { + const select=event.target; + if (select.tagName !== 'SELECT' || select.size > 1 || event.touches.length !== 1) {gesture=null;return;} + const point=event.touches[0]; + gesture={select,id:point.identifier,x:point.clientX,y:point.clientY,moved:false,at:performance.now()}; + },{capture:true,passive:true}); + addEventListener('touchmove', event => { + if (!gesture) return; + const point=[...event.touches].find(p=>p.identifier===gesture.id); + if (!point || event.touches.length!==1 || Math.hypot(point.clientX-gesture.x,point.clientY-gesture.y)>10) gesture.moved=true; + },{capture:true,passive:true}); + addEventListener('touchcancel',()=>{gesture=null;},{capture:true,passive:true}); + addEventListener('touchend', event => { + const tap=gesture;gesture=null; + if (!tap || tap.moved || performance.now()-tap.at>600 || event.touches.length) return; + const point=[...event.changedTouches].find(p=>p.identifier===tap.id); + if (!point || Math.hypot(point.clientX-tap.x,point.clientY-tap.y)>10) return; + event.preventDefault();event.stopImmediatePropagation(); + recentTap={select:tap.select,at:performance.now()};open(tap.select); + },{capture:true,passive:false}); + // Cancel the browser-chrome popup. Mouse and keyboard activation use Gaia too. + for (const type of ['mousedown','pointerdown']) addEventListener(type, event => { + if (event.target.tagName === 'SELECT' && event.target.size <= 1) event.preventDefault(); + }, true); + addEventListener('click', event => { + if (event.target.tagName !== 'SELECT' || event.target.size > 1) return; + event.preventDefault(); event.stopImmediatePropagation(); + if (recentTap?.select===event.target && performance.now()-recentTap.at<700) return; + open(event.target); + }, true); + addEventListener('keydown', event => { + if (active && event.key === 'Escape') {event.preventDefault();close(false);} + else if (event.target.tagName === 'SELECT' && [' ','Enter','ArrowDown'].includes(event.key)) { + event.preventDefault();open(event.target); + } + }, true); + addEventListener('pagehide', () => close(false)); + document.addEventListener('visibilitychange', () => {if (document.hidden) close(false);}); + addEventListener('panelready', event => { + // A late transition event for the same panel must not dismiss a menu + // opened immediately after navigation. Leaving the panel still cancels it. + if (active && event.detail?.current !== '#' + active.select.closest('section')?.id) close(false); + }); +})(); + +// Date/time pickers still belong to Gaia System. Embedded Gecko does not +// provide the retired mozInputMethod value-selector events for these inputs. +(() => { + const systemOrigin = 'http://system.localhost:8765'; + if (location.origin === systemOrigin) { + addEventListener('message', event => { + if (event.data?.type === 'vulpes-keyboard-close' && event.origin === 'http://keyboard.localhost:8765') { + const frame=[...document.querySelectorAll('iframe')].find(f=>f.contentWindow===event.source && f.src.startsWith(event.origin+'/settings.html')); + if(frame) { + const keyboard=Service.query('getApp',event.origin); + const close=()=>frame.isConnected && frame.dispatchEvent(new CustomEvent('mozbrowserclose',{bubbles:true})); + // Closing a still-transitioning background window can send Gaia + // Home over the application which has just replaced it. + if(keyboard?.isActive())keyboard.element.addEventListener('_closed',close,{once:true}); + else close(); + } + return; + } + const app = window.Service?.query('getActiveApp'); + if (!app || event.source !== app.iframe?.contentWindow || event.origin !== app.origin) return; + if (event.data?.type !== 'vulpes-datetime-open' || !['date','time'].includes(event.data.inputType)) return; + const picker = app.valueSelector; + if (!picker) return; + const previous = picker._im; + const reply = value => event.source.postMessage({type:'vulpes-datetime-result', id:event.data.id, value}, event.origin); + picker._im = { + setValue: value => reply(value), + removeFocus() { reply(null); picker._im = previous; } + }; + picker.handleEvent({type:'_inputfocus', detail:{inputType:event.data.inputType, + value:String(event.data.value || '').slice(0,32), min:event.data.min, max:event.data.max}}); + }); + return; + } + if (location.hostname === 'keyboard.localhost' && /settings/.test(location.pathname)) { + window.close = () => parent.postMessage({type:'vulpes-keyboard-close'}, systemOrigin); + } + let pending, sequence=0; + addEventListener('click', event => { + const input=event.target; + if (input.tagName !== 'INPUT' || !['date','time'].includes(input.type) || input.disabled || input.readOnly) return; + event.preventDefault();event.stopImmediatePropagation(); + pending={input,id:++sequence}; + parent.postMessage({type:'vulpes-datetime-open',id:sequence,inputType:input.type, + value:input.value,min:input.min,max:input.max},systemOrigin); + },true); + addEventListener('message', event => { + if(event.source!==parent || event.origin!==systemOrigin || event.data?.type!=='vulpes-datetime-result' || event.data.id!==pending?.id)return; + const {input}=pending;pending=null; + if(event.data.value!==null && input.isConnected && !input.disabled) { + input.value=event.data.value; + if(input.value) { + input.dispatchEvent(new Event('input',{bubbles:true})); + input.dispatchEvent(new Event('change',{bubbles:true})); + } + } + input.blur(); + }); +})(); + +// Modern native select text metrics differ from Gaia's 4rem button box. +// Keep the real select for interaction and accessibility, and paint its label +// in a regular line box so Fira's descenders are not clipped by the control. +(() => { + const labels=new WeakMap(); + function refresh() { + for(const select of document.querySelectorAll('.button select,.bb-button select')) { + if(select.multiple || select.size>1)continue; + let label=labels.get(select); + if(!label) { + label=document.createElement('span');label.className='vulpes-select-label'; + label.setAttribute('aria-hidden','true');select.after(label);labels.set(select,label); + select.classList.add('vulpes-select-label-owner'); + select.addEventListener('change',refresh); + } + const text=select.selectedOptions[0]?.textContent || ''; + if(label.textContent!==text)label.textContent=text; + } + } + let queued=false; + new MutationObserver(()=>{if(!queued){queued=true;queueMicrotask(()=>{queued=false;refresh();});}}) + .observe(document.documentElement,{childList:true,subtree:true,characterData:true,attributes:true,attributeFilter:['selected','value']}); + addEventListener('panelready',refresh);addEventListener('localized',refresh); + refresh(); +})(); diff --git a/host/gaia-services.js b/host/gaia-services.js index 238282e8..de360e80 100644 --- a/host/gaia-services.js +++ b/host/gaia-services.js @@ -18,6 +18,21 @@ function unavailable(message) { window.VulpesGaiaUI?.notice(message); } + if(location.hostname==='system.localhost') { + const colorKeys=['accessibility.colors.enable','accessibility.colors.invert','accessibility.colors.grayscale','accessibility.colors.contrast']; + const values={}; + function colors() { + const screen=document.getElementById('screen');if(!screen)return; + const enabled=values[colorKeys[0]]===true; + const contrast=Number(values[colorKeys[3]])||0; + screen.style.filter=enabled ? `invert(${values[colorKeys[1]]?1:0}) grayscale(${values[colorKeys[2]]?1:0}) contrast(${Math.max(0,Math.min(2,1+contrast))})` : ''; + } + for(const key of colorKeys) { + navigator.mozSettings.addObserver(key,e=>{values[key]=e.settingValue;colors();}); + navigator.mozSettings.createLock().get(key).then(row=>{values[key]=row[key];colors();}); + } + addEventListener('DOMContentLoaded',colors,{once:true}); + } addEventListener('DOMContentLoaded', () => { if (location.hostname === 'system.localhost' && !window.VulpesPlatform) { const ids = ['wifi', 'bluetooth', 'data', 'airplane-mode']; @@ -72,6 +87,23 @@ ); } }); + if(['settings.localhost','system.localhost'].includes(location.hostname) && window.VulpesPlatform!=='android') { + const manager=Object.assign(new EventTarget(),{}), downloads=new Map(); + manager.getDownloads=async()=>{ + const rows=await call('downloads.request',{operation:'list'}); + return rows.map(row=>{ + let item=downloads.get(row.id); + if(!item) { + item=Object.assign(new EventTarget(),{pause:()=>call('downloads.request',{operation:'pause',id:row.id}),resume:()=>call('downloads.request',{operation:'resume',id:row.id})}); + downloads.set(row.id,item); + } + Object.assign(item,row,{vulpesDownload:true,getBlob:()=>call('downloads.request',{operation:'blob',id:row.id}),startTime:new Date(row.startTime)});return item; + }); + }; + manager.remove=item=>call('downloads.request',{operation:'remove',id:item.id}); + manager.clearAllDone=async()=>{for(const item of await manager.getDownloads())if(item.state==='succeeded')await manager.remove(item);}; + Object.defineProperty(navigator,'mozDownloadManager',{value:manager}); + } Object.defineProperty(navigator, 'mozAlarms', { value: { getAll: () => @@ -222,6 +254,15 @@ } window.VulpesCompat.cursor = cursor; if (location.hostname === 'sms.localhost') { + addEventListener('DOMContentLoaded', () => { + const send = document.getElementById('messages-send-button'); + if (!send) return; + send.setAttribute('data-vulpes-keep-input', ''); + // Keep the composer focused, including with Android's native keyboard. + // Sending remains a click action, so a cancelled gesture never sends. + for (const type of ['pointerdown', 'mousedown']) + send.addEventListener(type, event => event.preventDefault()); + }); Object.defineProperty(navigator, 'mozMobileConnections', { value: Object.freeze([]) }); const messages = new EventTarget(); for (const operation of ['getThreads', 'getMessages']) @@ -242,11 +283,10 @@ 'getSegmentInfoForText', ]) messages[operation] = (...args) => request(call('messages.request', { operation, args })); - const dated = value => value && typeof value==='object' && 'timestamp' in value ? - {...value,timestamp:new Date(value.timestamp)} : value; + const dated = raw => { const value=window.VulpesSettingsCodec.decode(raw);return value && typeof value==='object' && 'timestamp' in value ? + {...value,timestamp:new Date(value.timestamp)} : value; }; messages.getMessage = id => request(call('messages.request',{operation:'getMessage',args:[id]}).then(dated)); - messages.send = (recipients,body,options) => { - const send = recipient => { + const submit = (operation,args) => { let id, settle; const early = new Map(); const terminal = new Promise((resolve,reject)=>{settle=message=>{ @@ -260,7 +300,7 @@ else if(id===undefined && early.size<100) early.set(message.id,message); }; addEventListener('vulpes-service-event',listener); - const result=call('messages.request',{operation:'send',args:[recipient,body,options]}).then(message=>{ + const result=call('messages.request',{operation,args}).then(message=>{ if(message.delivery!=='sending') return dated(message); id=message.id; if(early.has(id)) settle(early.get(id)); @@ -268,9 +308,12 @@ return terminal; }).finally(()=>removeEventListener('vulpes-service-event',listener)); return request(result); - }; + }; + messages.send = (recipients,body,options) => { + const send=recipient=>submit('send',[recipient,body,options]); return Array.isArray(recipients) ? recipients.map(send) : send(recipients); }; + messages.sendMMS = (message,options) => submit('sendMMS',[message,options]); addEventListener('vulpes-service-event',({detail})=>{ const type = {'sms-received':'received','sms-sending':'sending','sms-sent':'sent','sms-failed':'failed'}[detail.type]; if (!type) return; @@ -281,17 +324,22 @@ Object.defineProperty(navigator, 'mozMobileMessage', { value: messages }); } if ( - ['gallery', 'music', 'video', 'camera', 'ringtones'].includes(location.hostname.split('.')[0]) + ['settings', 'gallery', 'music', 'video', 'camera', 'ringtones'].includes(location.hostname.split('.')[0]) ) { const stores = new Map(); function storage(type) { - if (!['pictures', 'music', 'videos'].includes(type)) return null; + if (!['pictures', 'music', 'videos', ...(location.hostname==='settings.localhost'?['apps','sdcard']:[])].includes(type)) return null; if (stores.has(type)) return stores.get(type); const s = new EventTarget(); s.storageName = 'sdcard'; s.default = true; s.canBeMounted = false; - const run = (operation, params = {}) => call('media.request', { type, operation, ...params }); + const run = (operation, params = {}) => { + if(!['apps','sdcard'].includes(type))return call('media.request', { type, operation, ...params }); + if(operation==='available')return call('storage.stats',{}).then(()=>'available'); + if(['freeSpace','usedSpace'].includes(operation))return call('storage.stats',{}).then(stats=>operation==='freeSpace'?stats.free:stats[type]); + return Promise.reject(new DOMException('Read-only storage statistics','NotSupportedError')); + }; for (const op of ['available', 'freeSpace', 'usedSpace']) s[op] = () => request(run(op)); s.get = (name) => request( diff --git a/host/gaia-styles.js b/host/gaia-styles.js index 01000cf2..1ece3fb7 100644 --- a/host/gaia-styles.js +++ b/host/gaia-styles.js @@ -109,6 +109,14 @@ // Only a custom element's type selector names the scope root. A div // scope still contains ordinary div descendants (notably Lockscreen). if (owner.localName.includes('-')) text = text.replace(tag, ':scope'); + if (owner.id) { + const rootId = '#' + CSS.escape(owner.id); + const pattern = new RegExp(rootId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?![\\w-])', 'g'); + // Old scoped sheets could style their owner by ID. Modern @scope + // requires :scope for that element (for example the volume HUD). + text = text.replace(/([^{}]+)\{/g, (rule, selector) => + selector.replace(pattern, ':scope') + '{'); + } text = text.replace(/\.-(?:host|content)(?![\w-])/g, (match) => ':scope' + match); style.textContent = '@scope ([data-vulpes-scope="' + id + '"]) {' + text + '}'; style.removeAttribute('scoped'); diff --git a/host/gaia-theme.css b/host/gaia-theme.css new file mode 100644 index 00000000..1d91c0bf --- /dev/null +++ b/host/gaia-theme.css @@ -0,0 +1,105 @@ +/* Keep Gaia geometry, icons and media intact; change the surfaces and ink. */ +:root[data-vulpes-theme="dark"] { + color-scheme: dark; + --vulpes-surface: #18232e; + --vulpes-raised: #253442; + --vulpes-text: #f1f5f9; + --vulpes-muted: #b8c8d8; + --vulpes-line: #526272; + --link-color: #79ceff; + --header-background: var(--vulpes-raised); + --header-color: var(--vulpes-text); + --header-title-color: var(--vulpes-text); + --header-button-color: var(--vulpes-text); + --header-icon-color: var(--vulpes-text); + --header-action-button-color: #79ceff; + --header-disabled-button-color: var(--vulpes-muted); +} +:root[data-vulpes-theme="dark"]:is([data-vulpes-app="settings"], + [data-vulpes-app="communications"], [data-vulpes-app="sms"], + [data-vulpes-app="email"], [data-vulpes-app="calendar"], + [data-vulpes-app="ringtones"]) { + --background: var(--vulpes-surface); + --text-color: var(--vulpes-text); + --title-color: var(--vulpes-text); + --label-color: var(--vulpes-text); + background: var(--vulpes-surface) !important; + color: var(--vulpes-text) !important; +} +:root[data-vulpes-theme="dark"] gaia-header { + --header-background: var(--vulpes-raised) !important; + --header-title-color: var(--vulpes-text) !important; + --header-action-button-color: #79ceff !important; +} +#display #vulpes-theme-row { + display: block !important; + min-height: 10rem; + height: auto !important; + padding-bottom: 1rem; +} +#vulpes-theme-row > p { + height: auto; + line-height: 2.4rem; + padding: 1rem 1.5rem; + transform: none; +} +:root[data-vulpes-theme="dark"][data-vulpes-app="settings"] h2 { + background-color: var(--vulpes-surface) !important; +} +:root[data-vulpes-theme="dark"]:is([data-vulpes-app="settings"], + [data-vulpes-app="communications"], [data-vulpes-app="sms"], + [data-vulpes-app="email"], [data-vulpes-app="calendar"], + [data-vulpes-app="ringtones"]) :is(body, section, [role="region"], + [data-type="list"], .view, .card, #main, #messages-container, #thread-list, + #contacts-list, #view-contacts-list, #no-contacts, #call-log-container, #messages-compose-form) { + background-color: var(--vulpes-surface) !important; + color: var(--vulpes-text) !important; +} +:root[data-vulpes-theme="dark"] :is(input:not([type="range"]):not([type="checkbox"]):not([type="radio"]), textarea, select, [contenteditable="true"]) { + background-color: var(--vulpes-raised) !important; + color: var(--vulpes-text) !important; + border-color: var(--vulpes-line) !important; + caret-color: #79ceff; +} +:root[data-vulpes-theme="dark"] :is(input, textarea)::placeholder { color: var(--vulpes-muted) !important; } +:root[data-vulpes-theme="dark"][data-vulpes-app="settings"] :is(li, li > a, li > a > span, li > p, li > small, label, .explanation, h1, h2) { + color: var(--vulpes-text) !important; + border-color: var(--vulpes-line) !important; +} +:root[data-vulpes-theme="dark"][data-vulpes-app="settings"] :is(ul, header, .button, button:not(.danger)) { + background-color: var(--vulpes-raised) !important; + color: var(--vulpes-text) !important; +} +:root[data-vulpes-theme="dark"][data-vulpes-app="communications"] :is(.contact-item, .contact-item a, .contact-item p, #call-log-container li, #call-log-container a, #call-log-container p, .phone-number, #keypad-phone-number, .keypad-key) { + color: var(--vulpes-text) !important; +} +:root[data-vulpes-theme="dark"][data-vulpes-app="communications"] #phone-number-view-container { + background-color: var(--vulpes-raised) !important; +} +:root[data-vulpes-theme="dark"][data-vulpes-app="communications"] :is(.keypad-subicon, [role="tab"]) { + color: #79ceff !important; +} +:root[data-vulpes-theme="dark"][data-vulpes-app="communications"] [role="tab"][aria-selected="true"] { + background-color: var(--vulpes-raised) !important; +} +:root[data-vulpes-theme="dark"][data-vulpes-app="communications"] #no-contacts :is(p, .info) { + color: var(--vulpes-muted) !important; +} +:root[data-vulpes-theme="dark"][data-vulpes-app="communications"] #no-contacts { + background-blend-mode: soft-light; +} +:root[data-vulpes-theme="dark"][data-vulpes-app="sms"] :is(.threadlist-item, .threadlist-item a, .threadlist-item p, .time, .name, .body-text) { + color: var(--vulpes-text) !important; +} +:root[data-vulpes-theme="dark"][data-vulpes-app="sms"] .bubble { + background-color: var(--vulpes-raised) !important; + color: var(--vulpes-text) !important; +} +:root[data-vulpes-theme="dark"] :is([data-type="confirm"], [data-type="action"]) :is(section, menu, header, p) { + background-color: var(--vulpes-raised) !important; + color: var(--vulpes-text) !important; +} +:root[data-vulpes-theme="dark"][data-vulpes-app="system"] :is(#utility-tray, #notifications-container) { + background-color: var(--vulpes-surface) !important; + color: var(--vulpes-text) !important; +} diff --git a/host/gaia-theme.js b/host/gaia-theme.js new file mode 100644 index 00000000..f6745cf3 --- /dev/null +++ b/host/gaia-theme.js @@ -0,0 +1,58 @@ +// Shared appearance preference; external browser pages are not injected. +(() => { + const key = 'vulpes.theme'; + const system = matchMedia('(prefers-color-scheme: dark)'); + let mode = 'light', select; + function apply(value) { + mode = ['light', 'dark', 'system'].includes(value) ? value : 'light'; + document.documentElement.dataset.vulpesTheme = + mode === 'system' ? (system.matches ? 'dark' : 'light') : mode; + if (select) select.value = mode; + } + system.addEventListener('change', () => apply(mode)); + addEventListener('vulpes-service-event', ({detail}) => { + if (detail.type === 'settingchange' && detail.data.key === key) apply(detail.data.value); + }); + VulpesCompat.call('appearance.current', {}).then(apply).catch(console.error); + + if (location.hostname !== 'settings.localhost') return; + function label() { + const lang = document.documentElement.lang || navigator.language; + const labels = lang.startsWith('fr') ? ['Apparence', 'Clair', 'Sombre', 'Selon le système'] : + lang.startsWith('el') ? ['Εμφάνιση', 'Φωτεινό', 'Σκοτεινό', 'Σύμφωνα με το σύστημα'] : + ['Appearance', 'Light', 'Dark', 'Follow system']; + const title = document.getElementById('vulpes-theme-label'); + if (title && title.textContent !== labels[0]) title.textContent = labels[0]; + if (select) [...select.options].forEach((option, i) => { + if (option.textContent !== labels[i + 1]) option.textContent = labels[i + 1]; + }); + } + function insert() { + const list = document.querySelector('#display > div > ul'); + if (!list || select?.isConnected) return; + const row = document.createElement('li'), title = document.createElement('p'); + row.id = 'vulpes-theme-row'; + title.id = 'vulpes-theme-label'; + const wrapper = document.createElement('span'); + wrapper.className = 'button icon icon-dialog'; + select = document.createElement('select'); + select.id = 'vulpes-theme'; + select.setAttribute('aria-labelledby', title.id); + for (const value of ['light', 'dark', 'system']) { + const option = document.createElement('option'); + option.value = value; select.append(option); + } + select.value = mode; + select.onchange = async () => { + select.disabled = true; + try { await navigator.mozSettings.createLock().set({[key]:select.value}); } + catch (error) { select.value = mode; console.error(error); } + finally { select.disabled = false; } + }; + wrapper.append(select); row.append(title, wrapper); list.append(row); label(); + } + addEventListener('localized', label); + new MutationObserver(() => { insert(); label(); }).observe(document.documentElement, + {childList:true, subtree:true, attributes:true, attributeFilter:['lang']}); + insert(); +})(); diff --git a/host/gaia-wifi.js b/host/gaia-wifi.js index e2d81f9d..9de90ff4 100644 --- a/host/gaia-wifi.js +++ b/host/gaia-wifi.js @@ -6,7 +6,7 @@ enabled:false, connection:{status:'disconnected',network:null}, connectionInformation:null, macAddress:'', }); - let networks = []; + let networks = [], connectionInfo = {}; function emit(type, values = {}) { const event = Object.assign(new Event(type), values); manager.dispatchEvent(event); manager['on' + type]?.(event); @@ -21,7 +21,7 @@ const status = network ? 'connected' : 'disconnected'; const changed = manager.connection.status !== status || manager.connection.network?.ssid !== network?.ssid; manager.connection = {status, network}; - manager.connectionInformation = network ? {relSignalStrength:network.relSignalStrength} : null; + manager.connectionInformation = network ? {...connectionInfo,relSignalStrength:network.relSignalStrength} : null; if (changed) emit('statuschange', manager.connection); // Gaia's signal listeners require the associated network. A scan while // disconnected must not announce connection information for a missing AP. @@ -36,17 +36,23 @@ async function scanNetworks() { const result = await call('platform.wifi',{operation:'scan'}); power(result.enabled !== false); + connectionInfo=result.connectionInformation || {}; networks = result.networks.map(n => ({ssid:n.ssid, relSignalStrength:n.signal, - security:n.security === 'open' ? [] : [n.security === 'wpa-psk' ? 'WPA-PSK' : n.security === 'sae' ? 'SAE' : 'WPA-EAP'], + security:n.security === 'open' ? [] : [['wpa-psk','sae'].includes(n.security) ? 'WPA-PSK' : 'WPA-EAP'], known:n.known === true, connected:n.connected, vulpesId:n.id, savedId:n.savedId})); update(networks.find(n => n.connected) || null); return networks; } manager.getNetworks = () => request(scan()); - manager.getKnownNetworks = () => request(call('platform.wifi',{operation:'known'}).then(r=>r.networks.map(n=>({ssid:n.ssid,security:n.security==='open'?[]:['WPA-PSK'],known:true,savedId:n.id})))); + let knownPending; + manager.getKnownNetworks = () => { + if (!knownPending) knownPending = call('platform.wifi',{operation:'known'}).then(r=>r.networks.map(n=>({ssid:n.ssid,security:n.security==='open'?[]:['WPA-PSK'],known:true,savedId:n.id}))).finally(()=>{knownPending=null;}); + return request(knownPending); + }; manager.associate = network => request((async () => { - if (network.security.length && network.security[0] !== 'WPA-PSK') throw Error('WIFI_UNSUPPORTED_SECURITY'); - await call('platform.wifi',{operation:'connect',id:network.vulpesId,password:network.psk || ''}); + const security=(network.security || []).filter(Boolean); + if (security.length && security[0] !== 'WPA-PSK') throw Error('WIFI_UNSUPPORTED_SECURITY'); + await call('platform.wifi',network.hidden ? {operation:'connectHidden',ssid:network.ssid,security:security.length?'wpa-psk':'open',password:network.psk || ''} : {operation:'connect',id:network.vulpesId,password:network.psk || ''}); await scan(); return network; })()); manager.forget = network => request(call('platform.wifi',{operation:'forget',id:network.savedId}).then(()=>scan())); @@ -54,6 +60,91 @@ manager.wps = unsupported; manager.setPowerSavingMode = unsupported; Object.defineProperty(navigator,'mozWifiManager',{value:manager}); + if(location.hostname.startsWith('settings.')) { + const wifi = value => call('platform.wifi',value); + const french = () => (document.documentElement.lang || navigator.language || '').startsWith('fr'); + const message = (fr,en) => french() ? fr : en; + const note = panel => { + const p=document.createElement('p');p.className='explanation';p.setAttribute('role','status'); + panel.querySelector(':scope > div').prepend(p);return p; + }; + function hotspotPanel() { + let panel,toggle,link,status,state,busy=false; + function render() { + toggle.checked=state?.enabled===true;toggle.disabled=busy || !state; + link.setAttribute('aria-disabled',String(busy || !state || state.enabled)); + panel.querySelector('#wifi-hotspot-msg').textContent=state?.enabled ? state.ssid : ''; + } + async function refresh() { + busy=true;render(); + try {state=await wifi({operation:'hotspotStatus'});status.textContent=''; + await call('settings.set',{values:{'tethering.wifi.enabled':state.enabled}}); + } catch(error) {state=null;status.textContent=message('Impossible de lire le point d’accès Wi-Fi.','Cannot read Wi-Fi hotspot status.');} + finally {busy=false;render();} + } + return { + onInit(element) { + panel=element;toggle=panel.querySelector('#tethering-wifi-enabled');link=panel.querySelector('#hotspot-settings-section a');status=note(panel); + const usb=panel.querySelector('#tethering-usb-enabled');usb.disabled=true; + const explanation=document.createElement('p');explanation.className='explanation'; + explanation.textContent=message('Le partage par USB n’est pas encore disponible.','USB tethering is not available yet.');usb.closest('ul').append(explanation); + link.addEventListener('click',event=>{ + event.preventDefault();event.stopImmediatePropagation();if(busy || !state || state.enabled)return; + require(['modules/dialog_service'],DialogService=>DialogService.show('hotspot-wifiSettings',{state}).then(refresh)); + }); + toggle.addEventListener('change',async()=>{ + if(busy || !state)return; + if(!state.configured) {toggle.checked=false;status.textContent=message('Configurez d’abord le nom et le mot de passe du point d’accès.','Configure the hotspot name and password first.');return;} + const enabled=toggle.checked;busy=true;render(); + status.textContent=message('Modification du partage Wi-Fi…','Changing Wi-Fi sharing…'); + try { + if(enabled && (await call('settings.get',{key:'airplaneMode.status'})).value==='enabled')throw Error('AIRPLANE_MODE'); + state=await wifi({operation:'hotspotPower',enabled}); + await call('settings.set',{values:{'tethering.wifi.enabled':state.enabled}});status.textContent=''; + } catch(error) { + try {state=await wifi({operation:'hotspotStatus'});}catch {state=null;} + status.textContent=error.message==='AIRPLANE_MODE' ? message('Désactivez le mode avion avant de partager la connexion.','Turn off airplane mode before sharing the connection.') : message('Le changement a échoué. Vérifiez la connexion réseau.','The change failed. Check the network connection.'); + } finally {busy=false;render();} + }); + },onBeforeShow:refresh + }; + } + function hotspotDialog() { + let panel,ssid,security,password,show,status,busy=false; + function visibility() { + const hidden=security.value==='open'; + panel.querySelector('.password').hidden=hidden;panel.querySelector('.password-description').hidden=hidden; + } + return { + onInit(element) { + panel=element;ssid=panel.querySelector('input[type=text]');security=panel.querySelector('select');password=panel.querySelector('input[name=password]');show=panel.querySelector('gaia-checkbox');status=note(panel); + // These credentials belong to NetworkManager, not the global settings database. + for(const control of [ssid,security,password])control.removeAttribute('data-setting'); + security.replaceChildren(new Option('WPA2','wpa2-psk'),new Option(message('Ouvert','Open'),'open')); + security.addEventListener('change',visibility); + show.addEventListener('change',()=>{password.type=show.checked?'text':'password';}); + }, + onBeforeShow(element,options) { + const state=options.state;ssid.value=state.ssid;security.value=state.security;password.value=state.password; + password.type='password';show.checked=false;status.textContent='';visibility(); + }, + async onSubmit() { + if(busy)throw Error('HOTSPOT_BUSY'); + const values={operation:'hotspotSave',ssid:ssid.value,security:security.value,password:password.value}; + if(!values.ssid || new TextEncoder().encode(values.ssid).length>32 || (values.security!=='open' && (new TextEncoder().encode(values.password).length<8 || new TextEncoder().encode(values.password).length>63))) { + status.textContent=message('Nom : 1 à 32 octets. Mot de passe WPA2 : 8 à 63 octets.','Name: 1–32 bytes. WPA2 password: 8–63 bytes.');throw Error('HOTSPOT_INVALID'); + } + busy=true;const controls=panel.querySelectorAll('input,select,button,gaia-checkbox');controls.forEach(c=>c.disabled=true); + try {await wifi(values);return {};} + catch(error) {status.textContent=message('Impossible d’enregistrer le point d’accès.','Cannot save the hotspot.');throw error;} + finally {busy=false;controls.forEach(c=>c.disabled=false);} + }, + onCancel() {if(busy)throw Error('HOTSPOT_BUSY');}, + onHide() {password.value='';} + }; + } + window.VulpesWifiSettings={panels:{hotspot:hotspotPanel},dialogs:{'hotspot-wifiSettings':hotspotDialog}}; + } addEventListener('DOMContentLoaded', async () => { try { const state = await call('platform.wifi',{operation:'status'}); @@ -67,6 +158,7 @@ try { const result=await call('platform.wifi',{operation:'setEnabled',enabled}); power(result.enabled,true); + if(!result.enabled)await call('settings.set',{values:{'tethering.wifi.enabled':false}}); } catch(error) { await call('settings.set',{values:{'wifi.enabled':manager.enabled}}); console.warn('Wi-Fi radio change failed',error); diff --git a/host/platform-ui.js b/host/platform-ui.js index 182d4947..241b2174 100644 --- a/host/platform-ui.js +++ b/host/platform-ui.js @@ -86,6 +86,18 @@ try { return await call('platform.' + operation, params); } catch (error) { failure(error); } }; + if (app === 'communications') { + // All Gaia call entry points use the same authorized host operation. + window.VulpesPlatformDial = async number => { + try { + return navigator.mozTelephony ? await navigator.mozTelephony.dial(number) : + await call('platform.dial', {number}); + } catch (error) { + failure(error); + throw error; + } + }; + } function button(id, french, english, action) { const b = text(document.createElement('button'), french, english); b.id = id; @@ -145,7 +157,10 @@ event.preventDefault(); event.stopImmediatePropagation(); const number = document.getElementById('phone-number-view')?.value; - if (number) run('dial', {number}); + if (number) { + if (window.CallHandler) CallHandler.call(number, 0); + else window.VulpesPlatformDial(number).catch(() => {}); + } }, true); async function initialize() { const caps = await capabilities; diff --git a/host/shell.js b/host/shell.js index 2c57bc75..cfc1615f 100644 --- a/host/shell.js +++ b/host/shell.js @@ -30,7 +30,15 @@ window.addEventListener('load', async () => { const pollKeys = async () => { try { const state = await IOUtils.readJSON('/run/tundra-buttons/events.json'); - if (state.session !== session) { session = state.session; sequence = state.sequence; } + if (state.session !== session) { + session = state.session; + // Keep events already present when the poller first sees a new + // service session. Setting the sequence to the file's final + // value silently dropped the first physical key presses after a + // Gecko reload. + const first = state.events?.[0]?.id; + sequence = Number.isInteger(first) ? first - 1 : 0; + } else if (Date.now()/1000-state.recordedAt < 5) { for (const event of state.events) if (event.id > sequence) { Runtime.broadcast('hardware-key', {type:event.type}, 'system'); @@ -46,7 +54,7 @@ window.addEventListener('load', async () => { parent: { esModuleURI: 'resource://vulpes/host/actors/VulpesParent.sys.mjs' }, child: { esModuleURI: 'resource://vulpes/host/actors/VulpesChild.sys.mjs', - events: { DOMDocElementInserted: { capture: true }, focusin:{capture:true}, pointerdown:{capture:true} }, + events: { DOMDocElementInserted: { capture: true }, focusin:{capture:true}, pointerdown:{capture:true}, click:{capture:true} }, }, allFrames: true, safeForUntrustedWebProcess: true, @@ -89,6 +97,11 @@ window.addEventListener('load', async () => { browser.style.width = width + 'px'; browser.style.height = height + 'px'; homeButton.style.width = landscape ? '44px' : window.innerWidth + 'px'; + // Flush the embedder's layout before notifying the remote document. + // On Wayland its initial viewport can otherwise remain 0x0 until an + // unrelated chrome layout query, leaving Gaia and video unpainted. + browser.getBoundingClientRect(); + browser.frameLoader?.requestUpdatePosition?.(); }; window.addEventListener('resize', resize); diff --git a/overrides/camera.gaiamobile.org/js/lib/dcf.js b/overrides/camera.gaiamobile.org/js/lib/dcf.js new file mode 100644 index 00000000..ba28ba98 --- /dev/null +++ b/overrides/camera.gaiamobile.org/js/lib/dcf.js @@ -0,0 +1,101 @@ +define(function(require, exports, module) { +'use strict'; + +// This handles the logic pertaining to the naming of files according +// to the Design rule for Camera File System +// * http://en.wikipedia.org/wiki/Design_rule_for_Camera_File_system + +/** + * Dependencies + */ + +var asyncStorage = require('asyncStorage'); +var debug = require('debug')('dcf'); +var format = require('format'); + +/** + * Locals + */ + +var dcfConfigLoaded = false; +var deferredArgs = null; +var defaultSeq = {file: 1, dir: 100}; +var dcfConfig = { + key: 'dcf_key', + seq: null, + postFix: 'MZLLA', + prefix: {video: 'VID_', image: 'IMG_'}, + ext: {video: window.VulpesVideoExtension || '3gp', image: 'jpg'} +}; + +exports.init = function() { + debug('initializing'); + exports.checkFileCounter(function() { + // We have a previous call to createDCFFilename that is waiting for + // a response, fire it again + if (deferredArgs) { + var args = deferredArgs; + exports.createDCFFilename(args.storage, args.type, args.callback); + deferredArgs = null; + } + + debug('initialized'); + }); +}; + +exports.checkFileCounter = function(callback) { + asyncStorage.getItem(dcfConfig.key, function(value) { + dcfConfigLoaded = true; + dcfConfig.seq = value ? value : defaultSeq; + + if (callback) { + callback(); + } + }); +}; + +exports.createDCFFilename = function(storage, type, callback) { + + // We havent loaded the current counters from indexedDB yet, defer + // the call + if (!dcfConfigLoaded) { + deferredArgs = {storage: storage, type: type, callback: callback}; + return; + } + + var dir = 'DCIM/' + dcfConfig.seq.dir + dcfConfig.postFix + '/'; + var filename = dcfConfig.prefix[type] + + format.padLeft(dcfConfig.seq.file, 4, '0') + '.' + + dcfConfig.ext[type]; + var filepath = dir + filename; + + // A file with this name may have been written by the user or + // our indexeddb sequence tracker was cleared, check we wont overwrite + // anything + var req = storage.get(filepath); + + // A file existed, we bump the directory then try to generate a + // new filename + req.onsuccess = function() { + dcfConfig.seq.file = 1; + dcfConfig.seq.dir += 1; + asyncStorage.setItem(dcfConfig.key, dcfConfig.seq, function() { + exports.createDCFFilename(storage, type, callback); + }); + }; + + // No file existed, we are good to go + req.onerror = function() { + if (dcfConfig.seq.file < 9999) { + dcfConfig.seq.file += 1; + } else { + dcfConfig.seq.file = 1; + dcfConfig.seq.dir += 1; + } + asyncStorage.setItem(dcfConfig.key, dcfConfig.seq, function() { + callback(filepath, filename, dir); + }); + }; +}; + +}); diff --git a/overrides/camera.gaiamobile.org/js/main.js b/overrides/camera.gaiamobile.org/js/main.js index e8443fe6..376b3c17 100644 --- a/overrides/camera.gaiamobile.org/js/main.js +++ b/overrides/camera.gaiamobile.org/js/main.js @@ -4265,7 +4265,7 @@ define('controllers/overlay', [ if (id !== overlay.id) { return; } - var closable = self.activity.pick && type !== 'request-fail'; + var closable = self.activity.pick || reason === 'request-fail'; var view = new OverlayView({ type: reason, closable: closable @@ -4274,6 +4274,10 @@ define('controllers/overlay', [ return; } overlay.view = view.appendTo(document.body).on('click:close-btn', function () { + if (reason === 'request-fail' && !self.activity.pick) { + self.updateOverlay(type, false); + return; + } self.app.emit('activitycanceled'); }); debug('inserted \'%s/%s\' overlay', type, reason); @@ -5452,6 +5456,9 @@ define('views/viewfinder', [ if (!preview) { return; } + // Recompute when the embedded app becomes visible or rotates. + this.getSize(); + if (!this.width || !this.height) { return; } var aspect; if (sensorAngle % 180 === 0) { this.container = { @@ -5702,6 +5709,7 @@ define('controllers/viewfinder', [ this.views.viewfinder.set('grid', 'off'); }; ViewfinderController.prototype.bindEvents = function () { + window.addEventListener('resize', this.configurePreview); this.views.viewfinder.on('fadedin', this.app.firer('viewfinder:visible')); this.views.viewfinder.on('fadedout', this.app.firer('viewfinder:hidden')); this.views.viewfinder.on('click', this.app.firer('viewfinder:click')); @@ -6280,167 +6288,313 @@ define('lib/bytes-to-pixels', [ return Math.round(uncompressedBytes / bytesPerPixel); }; }); -define('controllers/activity', [ - 'require', - 'exports', - 'module', - 'debug', - 'lib/bytes-to-pixels', - 'lib/bind-all' -], function (require, exports, module) { - 'use strict'; - var debug = require('debug')('controller:activity'); - var bytesToPixels = require('lib/bytes-to-pixels'); - var bindAll = require('lib/bind-all'); - module.exports = function (app) { - return new ActivityController(app); - }; - module.exports.ActivityController = ActivityController; - function ActivityController(app) { - bindAll(this); - this.app = app; - this.win = app.win; - this.settings = app.settings; - this.configure(); - this.bindEvents(); - debug('initialized'); +define("controllers/activity",["require", "exports", "module", "debug", "lib/bytes-to-pixels", "lib/bind-all"],function(require, exports, module) { +'use strict'; + +/** + * Dependencies + */ + +var debug = require('debug')('controller:activity'); +var bytesToPixels = require('lib/bytes-to-pixels'); +var bindAll = require('lib/bind-all'); + +/** + * Exports + */ + +module.exports = function(app) { return new ActivityController(app); }; +module.exports.ActivityController = ActivityController; + +/** + * Initialize new `ActivityController` + * + * @param {App} app + */ +function ActivityController(app) { + bindAll(this); + this.app = app; + this.win = app.win; + this.settings = app.settings; + this.configure(); + this.bindEvents(); + debug('initialized'); +} + +/** + * Supported activity types. + * + * @type {Object} + */ +ActivityController.prototype.types = { + pick: 'pick', + record: 'record' +}; + +/** + * Initial configuration. + * + * @private + */ +ActivityController.prototype.configure = function() { + this.name = this.getName(); + this.app.activity[this.name] = true; +}; + +/** + * Filter down pictureSizes and + * recorderProfiles to match activity + * parameters each time the settings + * are configured. + * + * @private + */ +ActivityController.prototype.bindEvents = function() { + this.app.on('activitycanceled', this.onActivityCanceled); + this.app.on('confirm:selected', this.onActivityConfirmed); + + // If an activity name was found, we must bind + // the listener straight away so we don't miss + // the event, otherwise we can bind more lazily. + if (this.name) { this.setupListener(); } + else { this.app.once('criticalpathdone', this.setupListener); } +}; + +ActivityController.prototype.setupListener = function() { + debug('setup listener'); + navigator.mozSetMessageHandler('activity', this.onMessage); + debug('listener setup'); +}; + +/** + * Get activity name from the hash fragment. + * + * This is used only so that some parts + * of the app can prepare for an incoming + * activity before the message arrives. + * + * @private + */ +ActivityController.prototype.getName = function() { + var hash = this.win.location.hash; + var name = hash && hash.substr(1); + return this.types[name]; +}; + +/** + * Responds to incoming activity message. + * + * Configures the mode then emits an + * event so that other parts of + * the app can update accordingly. + * + * @param {MozActivity} activity + */ +ActivityController.prototype.onMessage = function(activity) { + debug('incoming activity', activity); + var name = activity.source.name; + var supported = this.types[name]; + + // Exit if this activity isn't supported + if (!supported) { return; } + + var data = { + name: name, + maxPixelSize: this.getMaxPixelSize(activity), + maxFileSizeBytes: activity.source.data.maxFileSizeBytes + }; + + this.activity = activity; + this.configureMode(activity); + this.app.emit('activity', data); + this.app.emit('activity:' + name, data); +}; + +/** + * Configures the mode setting based + * on the parameters supplied by + * the incoming activity. + * + * @param {MozActivity} activity + * @private + */ +ActivityController.prototype.configureMode = function(activity) { + var type = activity.source.data.type; + var name = activity.source.name; + var modes = (name === 'pick') ? + this.getModesForPick(type) : + this.getModesForRecord(type); + + this.settings.mode.filterOptions(modes); + this.settings.mode.select(modes[0]); + debug('configured mode', modes); +}; + +/** + * Get a max pixel size estimate based + * on the parameters supplied by the + * incoming activity. + * + * Activities don't always supply pixel + * size restrictions. + * + * NOTE: There + * + * @param {MozActivity} activity + * @return {Number|null} + */ +ActivityController.prototype.getMaxPixelSize = function(activity) { + var data = activity.source.data; + var bytes = data.maxFileSizeBytes; + var maxPickPixelSize = this.settings.activity.get('maxPickPixelSize') || 0; + var maxPixelSize; + + // If bytes were specified then derive + // a maxPixelSize from that, else we + // calculate the maxPixelSize using + // supplied dimensions. + if (bytes) { + maxPixelSize = bytesToPixels(bytes); + } else if (data.width || data.height) { + maxPixelSize = this.getMaxPixelsFromSize(data); + } else { + maxPixelSize = maxPickPixelSize; + } + + // If the Camera app has been configured to have a max pixel size + // for pick activities, ensure we are at or below that value. + if (maxPickPixelSize > 0) { + maxPixelSize = Math.min(maxPixelSize, maxPickPixelSize); + } + + debug('maxPixelsSize: %s', maxPixelSize); + return maxPixelSize; +}; + +ActivityController.prototype.getMaxPixelsFromSize = function(size) { + var scale = this.settings.activity.get('maxPixelSizeScaleFactor'); + var aspect = 4 / 3; + + // In the event that only one dimension has + // been supplied, calculate the largest the + // other edge could be based on a 4:3 aspect. + var width = size.width || size.height * aspect; + var height = size.height || size.width * aspect; + var pixels = width * height; + + // Take the actual total number of + // pixels asked for and bump it by the + // `scale` to cover pictureSizes above + // the pixels asked for. We later resize + // the image post-capture to the exact size + // requested (data.width * data.height). + // + // This is to avoid us picking a pictureSize + // smaller than the number of pixels requested + // and then to scaling up post-capture, + // resulting in a shitty image. + return pixels * scale; +}; + +/** + * Parse types given by 'pick' activity + * and return a list of modes. + * + * @param {Array|String} types + * @return {Array} + */ +ActivityController.prototype.getModesForPick = function(types) { + types = [].concat(types || []); + var modes = []; + + types.forEach(function(item) { + var type = item.split('/')[0]; + var mode = type === 'image' ? 'picture' : type; + + if (modes.indexOf(mode) === -1) { + modes.push(mode); } - ActivityController.prototype.types = { - pick: 'pick', - record: 'record' - }; - ActivityController.prototype.configure = function () { - this.name = this.getName(); - this.app.activity[this.name] = true; - }; - ActivityController.prototype.bindEvents = function () { - this.app.on('activitycanceled', this.onActivityCanceled); - this.app.on('confirm:selected', this.onActivityConfirmed); - if (this.name) { - this.setupListener(); - } else { - this.app.once('criticalpathdone', this.setupListener); + }); + + if (modes.length === 0) { + modes = ['picture', 'video']; + } + + return modes; +}; + +/** + * Parse types given by 'record' activity + * and return a list of modes. + * + * @param {String} type + * @return {Array} + */ +ActivityController.prototype.getModesForRecord = function(type) { + return type === 'videos' ? + ['video', 'picture'] : + ['picture', 'video']; +}; + +/** + * Respond to activity cancelation + * events and send the error call + * via the original acitity object. + * + * @private + */ +ActivityController.prototype.onActivityCanceled = function() { + if (!this.activity) { return; } + this.activity.postError('pick cancelled'); +}; + +// TODO: Messy, tidy up! +ActivityController.prototype.onActivityConfirmed = function(newMedia) { + var self = this; + var activity = this.activity; + var media = { + blob: newMedia.blob + }; + + // In low end devices resizing can be slow. + // We display a spinner + this.app.showSpinner(); + + // Video + if (newMedia.isVideo) { + media.type = media.blob.type || 'video/3gpp'; + media.poster = newMedia.poster.blob; + activity.postResult(media); + this.app.clearSpinner(); + } + + // Image + else { + media.type = 'image/jpeg'; + + // We need to call resizeImageAndSave() unconditionally even if + // the image does not need to be resized because if the camera + // produces images with EXIF orientation, we need to rotate the + // image before returning it from the pick activity. If the image + // does not need to be resized or rotated, then resizeImageAndSave + // will return it unchanged, and will not re-save it. + require(['lib/resize-image-and-save'], function(resizeImageAndSave) { + resizeImageAndSave( + { + blob: newMedia.blob, + width: activity.source.data.width, + height: activity.source.data.height + }, + function onImageResized(resizedBlob) { + media.blob = resizedBlob; + activity.postResult(media); + self.app.clearSpinner(); } - }; - ActivityController.prototype.setupListener = function () { - debug('setup listener'); - navigator.mozSetMessageHandler('activity', this.onMessage); - debug('listener setup'); - }; - ActivityController.prototype.getName = function () { - var hash = this.win.location.hash; - var name = hash && hash.substr(1); - return this.types[name]; - }; - ActivityController.prototype.onMessage = function (activity) { - debug('incoming activity', activity); - var name = activity.source.name; - var supported = this.types[name]; - if (!supported) { - return; - } - var data = { - name: name, - maxPixelSize: this.getMaxPixelSize(activity), - maxFileSizeBytes: activity.source.data.maxFileSizeBytes - }; - this.activity = activity; - this.configureMode(activity); - this.app.emit('activity', data); - this.app.emit('activity:' + name, data); - }; - ActivityController.prototype.configureMode = function (activity) { - var type = activity.source.data.type; - var name = activity.source.name; - var modes = name === 'pick' ? this.getModesForPick(type) : this.getModesForRecord(type); - this.settings.mode.filterOptions(modes); - this.settings.mode.select(modes[0]); - debug('configured mode', modes); - }; - ActivityController.prototype.getMaxPixelSize = function (activity) { - var data = activity.source.data; - var bytes = data.maxFileSizeBytes; - var maxPickPixelSize = this.settings.activity.get('maxPickPixelSize') || 0; - var maxPixelSize; - if (bytes) { - maxPixelSize = bytesToPixels(bytes); - } else if (data.width || data.height) { - maxPixelSize = this.getMaxPixelsFromSize(data); - } else { - maxPixelSize = maxPickPixelSize; - } - if (maxPickPixelSize > 0) { - maxPixelSize = Math.min(maxPixelSize, maxPickPixelSize); - } - debug('maxPixelsSize: %s', maxPixelSize); - return maxPixelSize; - }; - ActivityController.prototype.getMaxPixelsFromSize = function (size) { - var scale = this.settings.activity.get('maxPixelSizeScaleFactor'); - var aspect = 4 / 3; - var width = size.width || size.height * aspect; - var height = size.height || size.width * aspect; - var pixels = width * height; - return pixels * scale; - }; - ActivityController.prototype.getModesForPick = function (types) { - types = [].concat(types || []); - var modes = []; - types.forEach(function (item) { - var type = item.split('/')[0]; - var mode = type === 'image' ? 'picture' : type; - if (modes.indexOf(mode) === -1) { - modes.push(mode); - } - }); - if (modes.length === 0) { - modes = [ - 'picture', - 'video' - ]; - } - return modes; - }; - ActivityController.prototype.getModesForRecord = function (type) { - return type === 'videos' ? [ - 'video', - 'picture' - ] : [ - 'picture', - 'video' - ]; - }; - ActivityController.prototype.onActivityCanceled = function () { - if (!this.activity) { - return; - } - this.activity.postError('pick cancelled'); - }; - ActivityController.prototype.onActivityConfirmed = function (newMedia) { - var self = this; - var activity = this.activity; - var media = { blob: newMedia.blob }; - this.app.showSpinner(); - if (newMedia.isVideo) { - media.type = 'video/3gpp'; - media.poster = newMedia.poster.blob; - activity.postResult(media); - this.app.clearSpinner(); - } else { - media.type = 'image/jpeg'; - require(['lib/resize-image-and-save'], function (resizeImageAndSave) { - resizeImageAndSave({ - blob: newMedia.blob, - width: activity.source.data.width, - height: activity.source.data.height - }, function onImageResized(resizedBlob) { - media.blob = resizedBlob; - activity.postResult(media); - self.app.clearSpinner(); - }); - }); - } - }; + ); + }); + } +}; + }); define('controllers/camera', [ 'require', @@ -6792,354 +6946,606 @@ define('controllers/camera', [ } }; }); -define('lib/storage', [ - 'require', - 'exports', - 'module', - 'debug', - 'lib/bind-all', - 'evt' -], function (require, exports, module) { - 'use strict'; - var debug = require('debug')('storage'); - var bindAll = require('lib/bind-all'); - var events = require('evt'); - var storageSingleton; - module.exports = Storage; - events(Storage.prototype); - function Storage(options) { - if (storageSingleton) { - return storageSingleton; - } - storageSingleton = this; - bindAll(this); - this.maxFileSize = 0; - options = options || {}; - this.dcf = options.dcf; - if (this.dcf) { - this.dcf.init(); - } - this.require = options.require || require; - navigator.mozSettings.addObserver('device.storage.writable.name', this.onStorageVolumeChanged); - this.configure(); - debug('initialized'); +define("lib/storage",["require", "exports", "module", "debug", "lib/bind-all", "evt"],function(require, exports, module) { +'use strict'; + +/** + * Dependencies + */ + +var debug = require('debug')('storage'); +var bindAll = require('lib/bind-all'); +var events = require('evt'); +var storageSingleton; + +/** + * Expose `Storage` + */ + +module.exports = Storage; + +/** + * Mixin event emitter + */ + +events(Storage.prototype); + +/** + * Initialize a new `Storage`. + * + * @param {Object} options + */ +function Storage(options) { + if (storageSingleton) { + return storageSingleton; + } + storageSingleton = this; + bindAll(this); + this.maxFileSize = 0; + options = options || {}; + this.dcf = options.dcf; + if (this.dcf) { + this.dcf.init(); + } + this.require = options.require || require; + navigator.mozSettings.addObserver( + 'device.storage.writable.name', + this.onStorageVolumeChanged); + this.configure(); + debug('initialized'); +} + +Storage.prototype.createFilename = function(storage, type, done) { + var self = this; + this.require(['asyncStorage', 'lib/dcf'], function(as, dcf) { + if (!self.dcf) { + self.dcf = dcf; + self.dcf.init(); } - Storage.prototype.createFilename = function (storage, type, done) { - var self = this; - this.require([ - 'asyncStorage', - 'lib/dcf' - ], function (as, dcf) { - if (!self.dcf) { - self.dcf = dcf; - self.dcf.init(); - } - self.dcf.createDCFFilename(storage, type, done); - }); + + self.dcf.createDCFFilename(storage, type, done); + }); +}; + +/** + * Save the image Blob to DeviceStorage + * then lookup the File reference and + * return that in the callback as well + * as the resulting paths. + * + * You always want to forget about the + * Blob you told us about and use the + * File instead since otherwise you + * are wasting precious memory. + * + * @param {Object} [options] + * @param {String} options.filepath + * The path to save the image to. + */ +Storage.prototype.addPicture = function(blob, options, done) { + if (typeof options === 'function') { + done = options; + options = {}; + } + + done = done || function() {}; + var filepath = options && options.filepath; + var self = this; + debug('add picture', filepath); + + // Create a filepath if + // one hasn't been given. + if (!filepath) { + debug('creating filename'); + this.createFilename(this.picture, 'image', onCreated); + } else { + onCreated(filepath); + } + + function onCreated(filepath) { + var req = self.picture.addNamed(blob, filepath); + req.onerror = function() { self.emit('error'); }; + req.onsuccess = function(e) { + debug('image stored', filepath); + var absolutePath = e.target.result; + + // `addNamed` does not give us a File + // handle so we need to get() it again. + refetchFile(filepath, absolutePath); }; - Storage.prototype.addPicture = function (blob, options, done) { - if (typeof options === 'function') { - done = options; - options = {}; - } - done = done || function () { - }; - var filepath = options && options.filepath; - var self = this; - debug('add picture', filepath); - if (!filepath) { - debug('creating filename'); - this.createFilename(this.picture, 'image', onCreated); - } else { - onCreated(filepath); - } - function onCreated(filepath) { - var req = self.picture.addNamed(blob, filepath); - req.onerror = function () { - self.emit('error'); - }; - req.onsuccess = function (e) { - debug('image stored', filepath); - var absolutePath = e.target.result; - refetchFile(filepath, absolutePath); - }; - } - function refetchFile(filepath, absolutePath) { - var req = self.picture.get(filepath); - req.onerror = function () { - self.emit('error'); - done('Error adding picture to storage'); - }; - req.onsuccess = function (e) { - debug('image file blob handle retrieved'); - var fileBlob = e.target.result; - done(null, filepath, absolutePath, fileBlob); - }; - } + } + + function refetchFile(filepath, absolutePath) { + var req = self.picture.get(filepath); + req.onerror = function() { + self.emit('error'); + done('Error adding picture to storage'); }; - Storage.prototype.createVideoFilepath = function (done) { - var videoStorage = this.video; - var self = this; - this.createFilename(this.video, 'video', function (filepath) { - var dummyFilepath = getDir(filepath) + '.tmp.3gp'; - var blob = new Blob([''], { type: 'video/3gpp' }); - var req = videoStorage.addNamed(blob, dummyFilepath); - req.onerror = function (e) { - debug('Failed to add ' + filepath + ' to DeviceStorage', e); - var req = videoStorage.delete(dummyFilepath); - req.onerror = function () { - done('Error creating video file path'); - }; - req.onsuccess = function () { - self.createVideoFilepath(done); - }; - }; - req.onsuccess = function (e) { - videoStorage.delete(e.target.result); - done(null, filepath); - }; - }); + req.onsuccess = function(e) { + debug('image file blob handle retrieved'); + var fileBlob = e.target.result; + done(null, filepath, absolutePath, fileBlob); }; - Storage.prototype.onStorageChange = function (e) { - debug('state change: %s', e.reason); - var value = e.reason; - switch (value) { - case 'deleted': - var filepath = this.checkFilepath(e.path); - this.emit('itemdeleted', { path: filepath }); - break; - case 'available': - case 'shared': - case 'unavailable': - this.setState(value); - break; - } - this.check(); + } +}; + +/** + * Create a new video filepath. + * + * The CameraControl API will not + * automatically create directories + * for the new file if they do not + * exist. + * + * So we write a dummy file to the + * same directory via DeviceStorage + * to ensure that the directory exists + * before attempting to record to this + * filepath. + * + * If the request errors it can mean that + * there is a file that already exists + * in that name. If so, we attempt to delete + * it and try to create the filepath again. + * + * @param {Function} done + * @public + */ +Storage.prototype.createVideoFilepath = function(done) { + var videoStorage = this.video; + var self = this; + + this.createFilename(this.video, 'video', function(filepath) { + var dummyFilepath = getDir(filepath) + '.tmp.3gp'; + var blob = new Blob([''], { type: 'video/3gpp' }); + var req = videoStorage.addNamed(blob, dummyFilepath); + + req.onerror = function(e) { + debug('Failed to add ' + filepath + ' to DeviceStorage', e); + var req = videoStorage.delete(dummyFilepath); + req.onerror = function() { done('Error creating video file path'); }; + req.onsuccess = function() { self.createVideoFilepath(done); }; }; - Storage.prototype.configure = function (storageVolumeName) { - var i; - var videosStorages; - var picturesStorages; - if (this.picture) { - this.picture.removeEventListener('change', this.onStorageChange); - } - if (!storageVolumeName) { - this.video = navigator.getDeviceStorage('videos'); - this.picture = navigator.getDeviceStorage('pictures'); - } else { - videosStorages = navigator.getDeviceStorages('videos'); - this.video = videosStorages[0]; - for (i = 0; i < videosStorages.length; ++i) { - if (videosStorages[i].storageName === storageVolumeName) { - this.video = videosStorages[i]; - break; - } - } - picturesStorages = navigator.getDeviceStorages('pictures'); - this.picture = picturesStorages[0]; - for (i = 0; i < picturesStorages.length; ++i) { - if (picturesStorages[i].storageName === storageVolumeName) { - this.picture = picturesStorages[i]; - break; - } - } - } - if (!this.picture) { - this.setState('unavailable'); - return; - } - this.picture.addEventListener('change', this.onStorageChange); - this.emit('volumechanged', { - video: this.video, - picture: this.picture - }); + + req.onsuccess = function(e) { + videoStorage.delete(e.target.result); + done(null, filepath); }; - Storage.prototype.onStorageVolumeChanged = function (setting) { - debug('default storage volume change: %s', setting.settingValue); - this.configure(setting.settingValue); - }; - Storage.prototype.checkFilepath = function (filepath) { - var startString = filepath.indexOf('DCIM/'); - if (startString < -1) { - return; - } else if (startString > 0) { - filepath = filepath.substr(startString); - } - if (filepath.indexOf('VID') != -1 && filepath.lastIndexOf('.jpg') === filepath.length - 4) { - filepath = filepath.replace('.jpg', '.3gp'); - } - return filepath; - }; - Storage.prototype.setState = function (value) { - this.state = value; - debug('set state: %s', value); - this.emit('changed', value); - }; - Storage.prototype.setMaxFileSize = function (maxFileSize) { - this.maxFileSize = maxFileSize; - this.check(); - debug('max file size set: %d', maxFileSize); - }; - Storage.prototype.check = function (done) { - debug('check'); - var self = this; - done = done || function () { - }; - this.getState(function (result) { - self.setState(result); - if (!self.available()) { - onComplete('unhealthy'); - return; - } - if (self.dcf) { - self.dcf.checkFileCounter(); - } - self.isSpace(function (result) { - if (!result) { - self.setState('nospace'); - } - onComplete('healthy'); - }); - }); - function onComplete(state) { - self.emit('checked', state); - } - }; - Storage.prototype.isSpace = function (done) { - var maxFileSize = this.maxFileSize; - this.picture.freeSpace().onsuccess = function (e) { - var freeSpace = e.target.result; - var result = freeSpace > maxFileSize; - debug('is space: %s', result, freeSpace, maxFileSize); - done(result); - }; - }; - Storage.prototype.getState = function (done) { - if (!this.picture) { - setTimeout(function () { - done('unavailable'); - }); - return; - } - this.picture.available().onsuccess = function (e) { - done(e.target.result); - }; - }; - Storage.prototype.available = function () { - return this.state === 'available'; - }; - Storage.prototype.deletePicture = function (filepath, done) { - var req = this.picture.delete(filepath); - req.onerror = function (e) { - var message = 'Failed to delete ' + filepath + ' from DeviceStorage:' + e.target.error; - console.warn(message); - done(message); - }; - if (done) { - req.onsuccess = function () { - done(null); - }; - } - }; - Storage.prototype.deleteVideo = function (filepath) { - var poster = filepath.replace('.3gp', '.jpg'); - this.video.delete(filepath).onerror = function (e) { - console.warn('Failed to delete', filepath, 'from DeviceStorage:', e.target.error); - }; - this.picture.delete(poster).onerror = function (e) { - console.warn('Failed to delete poster image', poster, 'for video', filepath, 'from DeviceStorage:', e.target.error); - }; - }; - function getDir(filepath) { - var index = filepath.lastIndexOf('/') + 1; - return index ? filepath.substring(0, index) : ''; + }); +}; + +Storage.prototype.onStorageChange = function(e) { + debug('state change: %s', e.reason); + var value = e.reason; + + switch (value) { + case 'deleted': + // Emit an `itemdeleted` event to + // allow parts of the UI to update. + var filepath = this.checkFilepath(e.path); + this.emit('itemdeleted', { path: filepath }); + break; + case 'available': + case 'shared': + case 'unavailable': + this.setState(value); + break; + } + + // Check storage + // has spare capacity + this.check(); +}; + +Storage.prototype.configure = function(storageVolumeName) { + var i; + var videosStorages; + var picturesStorages; + // If we had a previous ds for pictures, let's remove the observer + // we had set as well before fetching new ds. + if (this.picture) { + this.picture.removeEventListener('change', this.onStorageChange); + } + if (!storageVolumeName) { + this.video = navigator.getDeviceStorage('videos'); + this.picture = navigator.getDeviceStorage('pictures'); + } else { // We select the volumes with the passed name + videosStorages = navigator.getDeviceStorages('videos'); + this.video = videosStorages[0]; + for (i = 0; i < videosStorages.length; ++i) { + if (videosStorages[i].storageName === storageVolumeName) { + this.video = videosStorages[i]; + break; + } } + + picturesStorages = navigator.getDeviceStorages('pictures'); + this.picture = picturesStorages[0]; + for (i = 0; i < picturesStorages.length; ++i) { + if (picturesStorages[i].storageName === storageVolumeName) { + this.picture = picturesStorages[i]; + break; + } + } + } + + // Shouldn't happen? + if (!this.picture) { + this.setState('unavailable'); + return; + } + + this.picture.addEventListener('change', this.onStorageChange); + this.emit('volumechanged',{ + video: this.video, + picture: this.picture + }); +}; + +Storage.prototype.onStorageVolumeChanged = function(setting) { + debug('default storage volume change: %s', setting.settingValue); + this.configure(setting.settingValue); +}; + +Storage.prototype.checkFilepath = function(filepath) { + var startString = filepath.indexOf('DCIM/'); + + if (startString < -1) { return; } + else if (startString > 0) { + filepath = filepath.substr(startString); + } + + // Check whether filepath is a video poster image or not. If filepath + // contains 'VID' and ends with '.jpg', consider it a video poster + // image and get the video filepath by changing '.jpg' to '.3gp' + if (filepath.indexOf('VID') != -1 && + filepath.lastIndexOf('.jpg') === filepath.length - 4) { + filepath = filepath.replace('.jpg', '.' + (window.VulpesVideoExtension || '3gp')); + } + + return filepath; +}; + +Storage.prototype.setState = function(value) { + this.state = value; + debug('set state: %s', value); + this.emit('changed', value); +}; + +Storage.prototype.setMaxFileSize = function(maxFileSize) { + this.maxFileSize = maxFileSize; + this.check(); + debug('max file size set: %d', maxFileSize); +}; + +/** + * Run a full storage check. + * + * @param {Function} done + * @public + */ +Storage.prototype.check = function(done) { + debug('check'); + + var self = this; + done = done || function() {}; + + this.getState(function(result) { + self.setState(result); + + if (!self.available()) { + onComplete('unhealthy'); + return; + } + + // check file counter in dcf if it already exist. + if (self.dcf) { + self.dcf.checkFileCounter(); + } + self.isSpace(function(result) { + if (!result) { self.setState('nospace'); } + onComplete('healthy'); + }); + }); + + function onComplete(state) { + self.emit('checked', state); + } +}; + +/** + * Checks if there is enough space to + * accomdate the current `maxFileSize`. + * + * @param {Function} done + */ +Storage.prototype.isSpace = function(done) { + var maxFileSize = this.maxFileSize; + this.picture + .freeSpace() + .onsuccess = function(e) { + var freeSpace = e.target.result; + var result = freeSpace > maxFileSize; + debug('is space: %s', result, freeSpace, maxFileSize); + done(result); + }; +}; + +/** + * Get current storage state. + * + * @param {Function} done + */ +Storage.prototype.getState = function(done) { + if (!this.picture) { + setTimeout(function() { + done('unavailable'); + }); + return; + } + + this.picture + .available() + .onsuccess = function(e) { + done(e.target.result); + }; +}; + +/** + * States if sotrage is available. + * + * @return {Boolean} + */ +Storage.prototype.available = function() { + return this.state === 'available'; +}; + +/** + * Delete a picture. + * + * @param {String} filepath + */ +Storage.prototype.deletePicture = function(filepath, done) { + var req = this.picture.delete(filepath); + req.onerror = function(e) { + var message = 'Failed to delete ' + filepath + + ' from DeviceStorage:' + e.target.error; + console.warn(message); + done(message); + }; + + if (done) { + req.onsuccess = function() { + done(null); + }; + } +}; + +/** + * Delete a video and accompanying + * poster image. + * + * @param {String} filepath + */ +Storage.prototype.deleteVideo = function(filepath) { + var poster = filepath.replace(/\.(?:3gp|mp4|webm)$/, '.jpg'); + + this.video.delete(filepath).onerror = function(e) { + console.warn( + 'Failed to delete', filepath, + 'from DeviceStorage:', e.target.error); + }; + + this.picture.delete(poster).onerror = function(e) { + console.warn( + 'Failed to delete poster image', poster, + 'for video', filepath, + 'from DeviceStorage:', e.target.error); + }; +}; + +/** + * Get the directory from a filepath. + * + * @param {String} filepath + * @return {String} + */ +function getDir(filepath) { + var index = filepath.lastIndexOf('/') + 1; + return index ? filepath.substring(0, index) : ''; +} + }); -define('controllers/storage', [ - 'require', - 'exports', - 'module', - 'debug', - 'lib/bind-all', - 'lib/storage' -], function (require, exports, module) { - 'use strict'; - var debug = require('debug')('controller:storage'); - var bindAll = require('lib/bind-all'); - var Storage = require('lib/storage'); - module.exports = function (app) { - return new StorageController(app); - }; - module.exports.StorageController = StorageController; - function StorageController(app) { - bindAll(this); - this.app = app; - this.camera = app.camera; - this.settings = app.settings; - this.storage = app.storage || new Storage(); - this.bindEvents(); - this.configure(); - debug('initialized'); - } - StorageController.prototype.configure = function () { - this.storage.configure(); - this.camera.createVideoFilepath = this.storage.createVideoFilepath; - this.updateMaxFileSize(); - }; - StorageController.prototype.bindEvents = function () { - debug('bind events'); - this.settings.pictureSizes.on('change:selected', this.updateMaxFileSize); - this.app.on('previewgallery:deletepicture', this.storage.deletePicture); - this.app.on('previewgallery:deletevideo', this.storage.deleteVideo); - this.app.on('settings:configured', this.updateMaxFileSize); - this.app.on('camera:newimage', this.storePicture); - this.app.on('camera:newvideo', this.storeVideo); - this.app.on('visible', this.storage.check); - this.storage.on('volumechanged', this.app.firer('storage:volumechanged')); - this.storage.on('itemdeleted', this.app.firer('storage:itemdeleted')); - this.storage.on('changed', this.onChanged); - this.storage.on('checked', this.onChecked); - debug('events bound'); - }; - StorageController.prototype.onChanged = function (state) { - debug('changed: %s', state); - this.app.emit('storage:changed', state); - }; - StorageController.prototype.onChecked = function (value) { - debug('checked: %s', value); - this.app.emit('storage:checked', value); - this.app.emit('storage:checked:' + value); - }; - StorageController.prototype.storePicture = function (picture) { - var memoryBlob = picture.blob; - var self = this; - this.storage.addPicture(memoryBlob, function (error, filepath, abspath, fileBlob) { - picture.blob = fileBlob; - picture.filepath = filepath; - debug('stored picture', picture); - self.app.emit('newmedia', picture); - }); - }; - StorageController.prototype.storeVideo = function (video) { - debug('new video', video); - var poster = video.poster; - var self = this; - poster.filepath = video.filepath.replace('.3gp', '.jpg'); - video.isVideo = true; - this.storage.addPicture(poster.blob, { filepath: poster.filepath }, function (error, path, absolutePath, fileBlob) { - poster.blob = fileBlob; - debug('new video', video); - self.app.emit('newmedia', video); - }); - }; - StorageController.prototype.updateMaxFileSize = function () { - var pictureSize = this.settings.pictureSizes.selected('data'); - if (pictureSize) { - var bytes = pictureSize.width * pictureSize.height / 2 + 25000; - this.storage.setMaxFileSize(bytes); - debug('maxFileSize updated %s', bytes); - } - }; +define("controllers/storage",["require", "exports", "module", "debug", "lib/bind-all", "lib/storage"],function(require, exports, module) { +'use strict'; + +/** + * Dependencies + */ + +var debug = require('debug')('controller:storage'); +var bindAll = require('lib/bind-all'); +var Storage = require('lib/storage'); + +/** + * Exports + */ + +module.exports = function(app) { return new StorageController(app); }; +module.exports.StorageController = StorageController; + +/** + * Initialize a new `StorageController` + * + * @param {App} app + */ +function StorageController(app) { + bindAll(this); + this.app = app; + this.camera = app.camera; + this.settings = app.settings; + this.storage = app.storage || new Storage(); + this.bindEvents(); + this.configure(); + debug('initialized'); +} + +/** + * Initial configuration. + * + * Give the camera a way to create video filepaths. + * This is so that the camera can record videos + * directly to the final location without us + * having to move the video file from temporary, + * to final location at recording end. + * + * @private + */ +StorageController.prototype.configure = function() { + this.storage.configure(); + this.camera.createVideoFilepath = this.storage.createVideoFilepath; + this.updateMaxFileSize(); +}; + +/** + * Bind to relevant events. + * + * @private + */ +StorageController.prototype.bindEvents = function() { + debug('bind events'); + + // App + this.settings.pictureSizes.on('change:selected', this.updateMaxFileSize); + this.app.on('previewgallery:deletepicture', this.storage.deletePicture); + this.app.on('previewgallery:deletevideo', this.storage.deleteVideo); + this.app.on('settings:configured', this.updateMaxFileSize); + this.app.on('camera:newimage', this.storePicture); + this.app.on('camera:newvideo', this.storeVideo); + this.app.on('visible', this.storage.check); + + // Storage + this.storage.on('volumechanged', this.app.firer('storage:volumechanged')); + this.storage.on('itemdeleted', this.app.firer('storage:itemdeleted')); + this.storage.on('changed', this.onChanged); + this.storage.on('checked', this.onChecked); + debug('events bound'); +}; + +/** + * Relay storage state change events. + * + * @param {String} state + * @private + */ +StorageController.prototype.onChanged = function(state) { + debug('changed: %s', state); + this.app.emit('storage:changed', state); +}; + +/** + * Emit the outcome of a storage check + * so that other parts of the app + * can respond. + * + * @param {String} value + * @private + */ +StorageController.prototype.onChecked = function(value) { + debug('checked: %s', value); + this.app.emit('storage:checked', value); + this.app.emit('storage:checked:' + value); +}; + +/** + * Store a picture. + * + * In either case, save the memory-backed + * photo blob to device storage, retrieve + * the resulting File (blob) and pass that + * around instead of the original memory blob. + * + * This is critical for "pick" activity consumers + * where the memory-backed Blob is either highly + * inefficent or will almost-immediately become + * inaccesible, depending on the state of the + * platform. https://bugzil.la/982779 + * + * @param {Object} picture + * @private + */ +StorageController.prototype.storePicture = function(picture) { + var memoryBlob = picture.blob; + var self = this; + + this.storage.addPicture( + memoryBlob, + function(error, filepath, abspath, fileBlob) { + picture.blob = fileBlob; + picture.filepath = filepath; + debug('stored picture', picture); + self.app.emit('newmedia', picture); + }); +}; + +/** + * Store a video. + * + * We don't store the video blob like + * we do for images, as it is recorded + * directly to the final location. + * This is for memory reason. The poster + * is also automatically created. + * + * @param {Object} video + */ +StorageController.prototype.storeVideo = function(video) { + debug('new video', video); + var poster = video.poster; + var self = this; + + // Add the poster image to the image storage + poster.filepath = video.filepath.replace(/\.(?:3gp|mp4|webm)$/, '.jpg'); + video.isVideo = true; + + this.storage.addPicture( + poster.blob, + { filepath: poster.filepath }, + function(error, path, absolutePath, fileBlob) { + // Replace the memory-backed Blob with the DeviceStorage file-backed File. + // Note that "video" has a reference to "poster", so video previews will + // use this File. + poster.blob = fileBlob; + debug('new video', video); + self.app.emit('newmedia', video); + }); +}; + +/** + * Calculate and set a `maxFileSize` + * value on the storage instance. + * + * This is so that it can alert us when there + * isn't enough space left in storage + * to accomodate a new picture. + * + * It is very unlikely that a JPEG file will have a file size that is + * more than half a byte per pixel. There is some fixed EXIF overhead + * that is the same for small and large pictures, however, so we add + * an additional 25,000 bytes of padding. + * + * @private + */ +StorageController.prototype.updateMaxFileSize = function() { + var pictureSize = this.settings.pictureSizes.selected('data'); + /* Depending on when we get loaded, we may not have a picture size yet. */ + if (pictureSize) { + var bytes = (pictureSize.width * pictureSize.height / 2) + 25000; + this.storage.setMaxFileSize(bytes); + debug('maxFileSize updated %s', bytes); + } +}; + }); window.jsStarted = Date.now(); define('main', [ diff --git a/overrides/gallery.gaiamobile.org/js/MetadataParser.js b/overrides/gallery.gaiamobile.org/js/MetadataParser.js new file mode 100644 index 00000000..87fba101 --- /dev/null +++ b/overrides/gallery.gaiamobile.org/js/MetadataParser.js @@ -0,0 +1,713 @@ +'use strict'; +/* global + CONFIG_MAX_IMAGE_PIXEL_SIZE, + CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT, + CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH, + cropResizeRotate, + Downsample, + getImageSize, + getVideoRotation, + isPhone, + parseJPEGMetadata, + videostorage +*/ +/* exported + metadataParser +*/ + +// +// This file defines a single metadataParsers object. The two +// properties of this object are metadata parsing functions for image +// files and video files, intended for use with the MediaDB class. +// +// This file depends on JPEGMetadataParser.js and blobview.js +// +var metadataParser = (function() { + // If we generate our own thumbnails, aim for this size. + // Calculate needed size from longer side of the screen. + var THUMBNAIL_WIDTH = computeThumbnailWidth(); + var THUMBNAIL_HEIGHT = THUMBNAIL_WIDTH; + + function computeThumbnailWidth() { + // Make sure this works regardless of current device orientation + var width = window.innerWidth || screen.width; + var height = window.innerHeight || screen.height; + var portraitWidth = Math.max(1, Math.min(width, height)); + var landscapeWidth = Math.max(1, Math.max(width, height)); + var thumbnailsPerRowPortrait = isPhone ? 3 : 4; + var thumbnailsPerRowLandscape = isPhone ? 4 : 6; + return Math.round(window.devicePixelRatio * + Math.max(portraitWidth / thumbnailsPerRowPortrait, + landscapeWidth / thumbnailsPerRowLandscape)); + } + + var thumbnailSize = { + width: THUMBNAIL_WIDTH, + height: THUMBNAIL_HEIGHT + }; + + // Don't try to decode image files of unknown type if bigger than this + var MAX_UNKNOWN_IMAGE_FILE_SIZE = 0.5 * 1024 * 1024; // half a megabyte + + // If a jpeg does not have a suitable EXIF preview and has more than + // this number of pixels, then we create an external preview for it + var MAX_PREVIEWLESS_JPEG_SIZE = 5 * 1024 * 1024; // 5 megapixels + + // An element for loading images + var offscreenImage = new Image(); + + // The screen size. Preview images must be at least this big + // Note that we use screen.width instead of window.innerWidth because + // the window size is different when we are invoked for a pick activity + // (non-fullscreen) and when we are invoked normally (fullscreen). + var sw = screen.width * window.devicePixelRatio; + var sh = screen.height * window.devicePixelRatio; + + // Create a thumbnail size canvas, copy the or