diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md
index dc0345cc..16788a7d 100644
--- a/CHANGELOG.en.md
+++ b/CHANGELOG.en.md
@@ -4,9 +4,58 @@
Updates by date and platform. Within each group: Add, Change, Fix, Note, Test. Entries describe the state at their date; a fix in development requires an updated installed version.
+
+
+## 04/11/2026: 2.7-preview.24
+
+
+
+### 
+
+-   : Gaia shutdown animation now shows three blue rings without a background logo.
+
+### 
+
+-   : Preview 24 APK.
+
+### 
+
+-   : Desktop and Gaia source published in the repository; launch with `./run.sh`.
+
+### 
+
+-   : Native package available. Flashing fully erases `userdata`; boot is not qualified.
+
+[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.24)
+
+
+
+## 27/10/2026: 2.7-preview.23 — built, not published
+
+### All Platforms
+
+- [Fix][Home] End-of-page spacing, icon editing and web pinning improved.
+- [Fix][Keyboard] Hiding on focus changes and first-tap search input corrected.
+- [Fix][Settings] Selectors, shortcuts, permissions and battery estimates improved.
+- [Fix][Marketplace] Catalogue return restored; unsupported privileged apps now show a clear message.
+- [Fix][Camera] Photo/video switching and viewfinder recovery improved; hardware qualification remains.
+- [Fix][System] Gaia screenshots added and shutdown animation no longer has an overlaid logo.
+
+### Android / APK
+
+- [Test][Installation] APK installed and launched on Android 16 (API 36); Gaia reached its lock screen. Unlocking and app flows remain to be qualified.
+
+### Pixel 3a / Sargo
+
+- [Change][Startup] Boot waits and checks reduced; battery standby qualification remains in progress.
+- [Note][Testing] System image and RAM boot built and verified; transfer to the Pixel awaits reconnection.
+
+
+**Limitations :** Nothing was published to Forgejo; reconnect the Pixel for RAM-boot qualification.
+
-## 22/10/2026 : 2.7-preview.22
+## 23/10/2026 : 2.7-preview.22

@@ -33,7 +82,20 @@ Updates by date and platform. Within each group: Add, Change, Fix, Note, Test. E
### 
--   : This release does not include a new Pixel image. Preview 21 remains the latest published image; this Preview's changes are not installed on the phone.
+-   : Experimental Preview 22 package for Pixel 3a Sargo added to this release. Fastboot wrote the planned partitions and rebooted the phone; Gaia startup is not yet confirmed.
+-   : Flashing completely erases `userdata`, including the old OS, files, apps and settings. Back up before installation.
+
+
+
+## 26/10/2026 : Sargo startup and battery life — in development
+
+### 
+
+-   : Boot no longer rereads the 3 GiB system image to calculate SHA-256. Provenance remains checked during preparation; boot checks file type and size before mounting read-only. The HAL wait is capped at 8 seconds.
+-   : Tundra waits for HAL initialization to finish, then enables Qualcomm low-power states. RAM boot v23 confirms `sleep_disabled=N`; CPU0 spent 3.6–4.0 seconds in C3 over 5.2-second samples.
+-   : Gaia startup no longer waits for the network login barrier.
+-   : With the backlight off in RAM, `Isolated Web Co` still used 21%, `crtc_commit` 13% and Firefox 10% of one core. Battery was 29.5 °C and CPU around 38 °C; the phone was plugged in, so discharge rate was not measured.
+-   : Private v23 image booted in RAM without partition writes. Gaia process profiling and a longer unplugged measurement remain.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 71c0a4e6..1f9f98b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,9 +4,58 @@
Les nouveautés, par date et par plateforme. Dans chaque groupe : Add, Change, Fix, Note, Test. Les entrées décrivent la situation à leur date ; une correction en développement nécessite une nouvelle version installée.
+
+
+## 04/11/2026 : 2.7-preview.24
+
+
+
+### 
+
+-   : Animation d’arrêt Gaia avec trois anneaux bleus, sans logo en arrière-plan.
+
+### 
+
+-   : APK Preview 24.
+
+### 
+
+-   : Code Desktop et Gaia publiés dans le dépôt; lancement avec `./run.sh`.
+
+### 
+
+-   : Paquet natif disponible. Le flash efface entièrement `userdata`; démarrage non qualifié.
+
+[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.24)
+
+
+
+## 27/10/2026 : 2.7-preview.23 — construite, non publiée
+
+### Toutes les plateformes
+
+- [Fix][Accueil] Espacement de fin de page, édition des icônes et épinglage web repris.
+- [Fix][Clavier] Masquage au changement de focus et premier appui sur la recherche corrigés.
+- [Fix][Paramètres] Sélecteurs, raccourcis, permissions et estimation batterie améliorés.
+- [Fix][Marketplace] Retour catalogue restauré; les applications privilégiées non prises en charge sont signalées clairement.
+- [Fix][Caméra] Cycle photo/vidéo et reprise du viseur fiabilisés; qualification matérielle encore requise.
+- [Fix][Système] Capture d’écran Gaia et animation d’arrêt sans logo superposé.
+
+### Android / APK
+
+- [Test][Installation] APK installée et lancée sur Android 16 (API 36); Gaia a affiché son écran de verrouillage. Déverrouillage et parcours des apps restent à qualifier.
+
+### Pixel 3a / Sargo
+
+- [Change][Démarrage] Temps d’attente et vérifications au boot réduits; qualification batterie en veille toujours en cours.
+- [Note][Essais] Image système et boot RAM construits et vérifiés; transfert au Pixel en attente de sa reconnexion.
+
+
+**Limites :** Aucun changement Forgejo publié; le Pixel doit être reconnecté pour le démarrage de qualification.
+
-## 22/10/2026 : 2.7-preview.22
+## 23/10/2026 : 2.7-preview.22

@@ -33,7 +82,20 @@ Les nouveautés, par date et par plateforme. Dans chaque groupe : Add, Change, F
### 
--   : Cette release ne contient pas de nouvelle image Pixel. La Preview 21 reste la dernière image publiée ; les changements de cette Preview ne sont pas installés sur le téléphone.
+-   : Paquet expérimental Preview 22 pour Pixel 3a Sargo ajouté à la release. Le flash a écrit les partitions prévues et redémarré le téléphone ; le démarrage de Gaia n’a pas encore été confirmé.
+-   : Le flash efface intégralement `userdata`, y compris l’ancien système, les fichiers, applications et réglages. Sauvegarde requise avant installation.
+
+
+
+## 26/10/2026 : Démarrage Sargo et autonomie — en développement
+
+### 
+
+-   : Le boot ne relit plus les 3 Gio de l’image système en SHA-256. La provenance reste contrôlée à la préparation ; au démarrage, taille et type sont vérifiés avant le montage en lecture seule. L’attente HAL est plafonnée à 8 s.
+-   : Tundra attend la fin d’initialisation du HAL puis active les états basse consommation Qualcomm. Le boot RAM v23 confirme `sleep_disabled=N` ; CPU0 a résidé 3,6 à 4,0 s en C3 sur des fenêtres de 5,2 s.
+-   : Gaia ne dépend plus de l’attente de session réseau pour démarrer.
+-   : En RAM, l’écran éteint laisse encore `Isolated Web Co` à 21 %, `crtc_commit` à 13 % et Firefox à 10 % d’un cœur. Batterie à 29,5 °C et CPU autour de 38 °C ; téléphone branché, donc taux de décharge non mesuré.
+-   : Image v23 privée, démarrée en RAM sans écriture de partition. Le profilage des processus Gaia et la mesure prolongée sans câble restent à faire.
diff --git a/README.fr.md b/README.fr.md
index 30363294..53b1bfba 100644
--- a/README.fr.md
+++ b/README.fr.md
@@ -4,7 +4,7 @@
Firefox OS fork · Open source
-
+
@@ -38,11 +38,9 @@ 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 22 · 22 octobre 2026
+### Preview 24 · publiée
-[Télécharger l’APK Preview 22](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.22). Les corrections Gaia concernent le partage du navigateur et la saisie web ; les contrôles PC ont réussi. La release comprend aussi les sources Tundra/Nokia actuelles. L’installation et la mise à jour ont réussi sur émulateur Android 11 ; les téléphones Android réels restent à qualifier. Le clavier T9 du Nokia demande aussi un essai physique.
-
-Une image expérimentale pour le Pixel 3a (Sargo) est jointe à la Preview 22. Elle a été flashée, mais son démarrage n’est pas encore qualifié. **Le flash efface toutes les données de `userdata`, y compris l’ancien système, les fichiers, applications et réglages. Sauvegardez avant l’installation.** Consultez la [release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.22) et le [changelog](CHANGELOG.md).
+La Preview 24 réunit les sources Desktop, l’APK et une image expérimentale pour Pixel 3a. Le démarrage de cette image n’est pas qualifié; son installation efface entièrement `userdata`. Consultez le [changelog](CHANGELOG.md) et les avertissements de la [release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.24) avant toute installation.
PC/Sargo utilisent Gecko **156.0.1** ; Android utilise GeckoView **156.0**. Android 8 / API 26 minimum est déclaré. L’APK est un build de développement signé, avec débogage via ADB autorisé.
### Linux
diff --git a/README.md b/README.md
index 8c3306ca..bb9de9a4 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
Firefox OS fork · Open source
-
+
@@ -38,11 +38,9 @@ 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 22 · 22 October 2026
+### Preview 24 · released
-[Download the Preview 22 APK](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.22). Shared Gaia fixes cover browser sharing and web input; PC checks passed. The release also includes the current Tundra/Nokia sources. Android 11 emulator installation and upgrade passed; physical Android devices still need qualification. The Nokia T9 keyboard also needs physical qualification.
-
-An experimental Pixel 3a (Sargo) image is attached to Preview 22. It was flashed, but boot is not yet qualified. **Flashing erases all data in `userdata`, including the existing OS, files, apps and settings. Back up first.** See the [release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.22) and [changelog](CHANGELOG.en.md) for details.
+Preview 24 includes the Desktop source, APK and an experimental Pixel 3a image. Boot is not qualified; installation fully erases `userdata`. Read the [changelog](CHANGELOG.en.md) and the [release warnings](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.24) before installing.
Desktop/Sargo use Gecko **156.0.1**; Android uses GeckoView **156.0**. Android 8 / API 26 minimum is declared. The APK is a signed development build with debugging available through authorized ADB.
### Linux
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 94eeeacf..51916801 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
@@ -17,7 +17,7 @@ public final class MainActivity extends Activity {
void updateTrackingProtection(boolean enabled) {
if (gaia != null) gaia.getSettings().setUseTrackingProtection(enabled);
- if (web != null) web.getSettings().setUseTrackingProtection(enabled);
+ for (GeckoSession session : webTabs) session.getSettings().setUseTrackingProtection(enabled);
}
private VulpesApplication app;
@@ -26,8 +26,12 @@ public final class MainActivity extends Activity {
private LinearLayout root, webPanel;
private TextView notice;
private EditText address;
+ private Button tabsButton;
private Button home;
private boolean webBack;
+ private final java.util.List webTabs = new ArrayList<>();
+ private final java.util.List webUrls = new ArrayList<>();
+ private final java.util.List webCanGoBack = new ArrayList<>();
private boolean resumed, recoverGaia, recoverWeb;
private long lastCrash;
private int crashCount;
@@ -95,6 +99,13 @@ public final class MainActivity extends Activity {
address.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
address.setTextSize(14);
nav.addView(address, new LinearLayout.LayoutParams(0, -1, 1));
+ tabsButton = new Button(this);
+ tabsButton.setText("▢ 0");
+ tabsButton.setContentDescription("Onglets");
+ tabsButton.setMinWidth(0);
+ tabsButton.setPadding(dp(8), 0, dp(8), 0);
+ nav.addView(tabsButton, new LinearLayout.LayoutParams(dp(64), -1));
+ tabsButton.setOnClickListener(v -> showBrowserTabs());
address.setOnEditorActionListener((v, id, event) -> {
String url = address.getText().toString().trim();
openBrowser(url.contains("://") ? url : "https://" + url);
@@ -221,56 +232,99 @@ public final class MainActivity extends Activity {
} catch (Exception e) {
return;
}
- if (web == null) {
- web = new GeckoSession(new GeckoSessionSettings.Builder()
- .useTrackingProtection(app.trackingProtection).build());
- web.setContentDelegate(content);
- web.setPromptDelegate(prompts);
- web.setPermissionDelegate(permissions);
- web.setNavigationDelegate(
- new GeckoSession.NavigationDelegate() {
- @Override
- public void onCanGoBack(GeckoSession s, boolean can) {
- webBack = can;
- }
-
- @Override
- public void onLocationChange(
- GeckoSession s,
- String url,
- java.util.List permissions,
- Boolean gesture
- ) {
- address.setText(url);
- }
-
- @Override
- public GeckoResult onLoadRequest(GeckoSession s, LoadRequest r) {
- return GeckoResult.fromValue(
- (r.uri.startsWith("https://") || r.uri.startsWith("http://")) && !isGaia(r.uri)
- ? AllowOrDeny.ALLOW
- : AllowOrDeny.DENY
- );
- }
-
- @Override
- public GeckoResult onNewSession(GeckoSession s, String uri) {
- openBrowser(uri);
- return GeckoResult.fromValue(null);
- }
- }
- );
- web.open(app.runtime);
- webView.setSession(web);
+ if (web == null) newBrowserTab(url);
+ else {
+ int index = webTabs.indexOf(web);
+ if (index >= 0) webUrls.set(index, url);
+ web.loadUri(url);
+ updateTabsButton();
}
gaiaView.setVisibility(View.GONE);
webPanel.setVisibility(View.VISIBLE);
setHomeSurface(false);
address.setText(url);
- web.loadUri(url);
syncActive();
}
+ private GeckoSession createBrowserSession() {
+ GeckoSession session = new GeckoSession(new GeckoSessionSettings.Builder()
+ .useTrackingProtection(app.trackingProtection).build());
+ session.setContentDelegate(content);
+ session.setPromptDelegate(prompts);
+ session.setPermissionDelegate(permissions);
+ session.setNavigationDelegate(new GeckoSession.NavigationDelegate() {
+ @Override public void onCanGoBack(GeckoSession s, boolean can) {
+ int index = webTabs.indexOf(s);
+ if (index < 0) return;
+ webCanGoBack.set(index, can);
+ if (s == web) webBack = can;
+ }
+ @Override public void onLocationChange(GeckoSession s, String url,
+ java.util.List permissions,
+ Boolean gesture) {
+ int index = webTabs.indexOf(s);
+ if (index >= 0) webUrls.set(index, url);
+ if (s == web) address.setText(url);
+ updateTabsButton();
+ }
+ @Override public GeckoResult onLoadRequest(GeckoSession s, LoadRequest r) {
+ return GeckoResult.fromValue(
+ (r.uri.startsWith("https://") || r.uri.startsWith("http://")) && !isGaia(r.uri)
+ ? AllowOrDeny.ALLOW : AllowOrDeny.DENY);
+ }
+ @Override public GeckoResult onNewSession(GeckoSession s, String uri) {
+ newBrowserTab(uri);
+ return GeckoResult.fromValue(null);
+ }
+ });
+ session.open(app.runtime);
+ return session;
+ }
+
+ private void newBrowserTab(String url) {
+ if (web != null) web.setActive(false);
+ web = createBrowserSession();
+ webTabs.add(web);
+ webUrls.add(url);
+ webCanGoBack.add(false);
+ webView.setSession(web);
+ webBack = false;
+ address.setText(url);
+ web.loadUri(url);
+ updateTabsButton();
+ syncActive();
+ }
+
+ private void showBrowserTabs() {
+ java.util.List choices = new ArrayList<>();
+ choices.add("+ Nouvel onglet");
+ for (int i = 0; i < webUrls.size(); i++) {
+ String url = webUrls.get(i);
+ choices.add((webTabs.get(i) == web ? "✓ " : "") +
+ (url == null || url.isEmpty() ? "Nouvel onglet" : url));
+ }
+ new AlertDialog.Builder(this).setTitle("Onglets")
+ .setItems(choices.toArray(new String[0]), (dialog, which) -> {
+ if (which == 0) newBrowserTab("https://duckduckgo.com/");
+ else selectBrowserTab(which - 1);
+ }).setNegativeButton("Fermer", null).show();
+ }
+
+ private void selectBrowserTab(int index) {
+ if (index < 0 || index >= webTabs.size()) return;
+ if (web != null) web.setActive(false);
+ web = webTabs.get(index);
+ webView.setSession(web);
+ webBack = webCanGoBack.get(index);
+ address.setText(webUrls.get(index));
+ updateTabsButton();
+ syncActive();
+ }
+
+ private void updateTabsButton() {
+ if (tabsButton != null) tabsButton.setText("▢ " + webTabs.size());
+ }
+
void setHomeSurface(boolean visible) {
runOnUiThread(() -> {
boolean overlay = visible && webPanel.getVisibility() != View.VISIBLE;
@@ -334,7 +388,8 @@ public final class MainActivity extends Activity {
private void syncActive() {
boolean browsing = webPanel != null && webPanel.getVisibility() == View.VISIBLE;
if (gaia != null && gaia.isOpen()) gaia.setActive(resumed && !browsing);
- if (web != null && web.isOpen()) web.setActive(resumed && browsing);
+ for (GeckoSession session : webTabs)
+ if (session.isOpen()) session.setActive(resumed && browsing && session == web);
}
private void recover(GeckoSession session, boolean crashed) {
@@ -442,7 +497,7 @@ public final class MainActivity extends Activity {
app.gaiaSession = null;
}
}
- if (web != null) web.close();
+ for (GeckoSession session : webTabs) session.close();
if (fileResult != null) {
fileResult.complete(filePrompt.dismiss());
fileResult = null;
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 3eba174a..fba1130a 100644
--- a/assets/webapps/homescreen.gaiamobile.org/gaia_build_defer_index.js
+++ b/assets/webapps/homescreen.gaiamobile.org/gaia_build_defer_index.js
@@ -794,21 +794,21 @@ function fetchIconBlob(iconUrl){return new Promise((resolve,reject)=>{var xhr=ne
reject(new Error(`Got HTTP status ${xhr.status} trying to load ${iconUrl}.`));};xhr.onerror=xhr.ontimeout=()=>{reject(new Error(`Error while getting ${iconUrl}.`));};});}
exports.IconsHelper={getIcon:getIcon,getIconBlob:getIconBlob,setElementIcon:setElementIcon,getBestIconFromWebManifest:getBestIconFromWebManifest,getBestIconFromMetaTags:getBestIconFromMetaTags,fetchIcon:fetchIcon,fetchIconBlob:fetchIconBlob,get defaultIconSize(){return getDefaultIconSize();},clear:clear,getNearestSize:getNearestSize,};})(window);;'use strict';(function(exports){const SETTINGS_STORE='homescreen_settings';const SETTINGS_VERSION=0;const COLUMNS_SETTING='grid.cols';const PAGING_SETTING='grid.paging';function Settings(){this.small=false;this.scrollSnapping=false;this.firstRun=false;if(navigator.getDataStores){navigator.getDataStores(SETTINGS_STORE).then(stores=>{if(stores.length<1){return Promise.reject('Settings datastore inaccessible');}
var signalChange=()=>{this.save();window.dispatchEvent(new CustomEvent('settings-changed'));};var syncSmallSetting=(signal)=>{stores[0].get(COLUMNS_SETTING).then(cols=>{var oldSmall=this.small;this.small=cols>3;if(this.small!==oldSmall){signalChange();}});};var syncPagingSetting=()=>{stores[0].get(PAGING_SETTING).then(paging=>{var oldSnapping=this.scrollSnapping;this.scrollSnapping=paging||false;if(this.scrollSnapping!==oldSnapping){signalChange();}});};stores[0].addEventListener('change',e=>{switch(e.id){case COLUMNS_SETTING:syncSmallSetting();break;case PAGING_SETTING:syncPagingSetting();break;}});syncSmallSetting();syncPagingSetting();},e=>{console.error('Error retrieving home screen settings datastore:',e);});}else{console.error('Datastore API unavailable');}
-var settingsString=localStorage.getItem('settings');if(!settingsString){this.firstRun=true;return;}
+var settingsString;try{settingsString=localStorage.getItem('settings');}catch(e){console.error('Home screen local settings unavailable:',e);return;}if(!settingsString){this.firstRun=true;return;}
var settings=JSON.parse(settingsString);if(settings.version!==SETTINGS_VERSION){return;}
this.small=settings.small||false;this.scrollSnapping=settings.scrollSnapping||false;if(!navigator.getDataStores){console.error('Datastore API unavailable');return;}}
-Settings.prototype={save:function(){localStorage.setItem('settings',JSON.stringify({version:SETTINGS_VERSION,small:this.small,scrollSnapping:this.scrollSnapping}));}};exports.Settings=Settings;}(window));;'use strict';(function(exports){const DB_NAME='home-metadata';const DB_ORDER_STORE='order';const DB_ICON_STORE='icon';const DB_VERSION=1;function AppsMetadata(){}
+Settings.prototype={save:function(){try{localStorage.setItem('settings',JSON.stringify({version:SETTINGS_VERSION,small:this.small,scrollSnapping:this.scrollSnapping}));}catch(e){console.error('Home screen settings could not be saved:',e);}}};exports.Settings=Settings;}(window));;'use strict';(function(exports){const DB_NAME='home-metadata';const DB_ORDER_STORE='order';const DB_ICON_STORE='icon';const DB_VERSION=1;function AppsMetadata(){}
AppsMetadata.prototype={db:null,init:function(){return new Promise((resolve,reject)=>{var req=window.indexedDB.open(DB_NAME,DB_VERSION);req.onupgradeneeded=this.upgradeSchema;req.onsuccess=(e)=>{this.db=e.target.result;resolve();};req.onerror=(e)=>{console.error('Error opening homescreen metadata db',e);reject(e);};});},upgradeSchema:function(e){var db=e.target.result;var fromVersion=e.oldVersion;if(fromVersion<1){var store=db.createObjectStore(DB_ORDER_STORE,{keyPath:'id'});store.createIndex('order','order',{unique:false});store=db.createObjectStore(DB_ICON_STORE,{keyPath:'id'});store.createIndex('icon','icon',{unique:false});}},set:function(data){return new Promise((resolve,reject)=>{var txn=this.db.transaction([DB_ORDER_STORE,DB_ICON_STORE],'readwrite');for(var entry of data){if(!entry.id){continue;}
if(typeof entry.order!=='undefined'){txn.objectStore(DB_ORDER_STORE).put({id:entry.id,order:entry.order});}
if(typeof entry.icon!=='undefined'){txn.objectStore(DB_ICON_STORE).put({id:entry.id,icon:entry.icon});}}
txn.oncomplete=resolve;txn.onerror=reject;});},remove:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([DB_ORDER_STORE,DB_ICON_STORE],'readwrite');txn.objectStore(DB_ORDER_STORE).delete(id);txn.objectStore(DB_ICON_STORE).delete(id);txn.oncomplete=resolve;txn.onerror=reject;});},getAll:function(onResult){return new Promise((resolve,reject)=>{var txn=this.db.transaction([DB_ORDER_STORE,DB_ICON_STORE],'readonly');var orderStore=txn.objectStore(DB_ORDER_STORE);var iconStore=txn.objectStore(DB_ICON_STORE);var cursor=orderStore.index('order').openCursor();var results=[];cursor.onsuccess=e=>{var cursor=e.target.result;if(cursor){var result=cursor.value;var iconRequest=iconStore.get(result.id);iconRequest.onsuccess=function(result,e){if(e.target.result){result.icon=e.target.result.icon;}
results.push(result);if(onResult){onResult(result);}}.bind(this,result);cursor.continue();}};txn.oncomplete=()=>{resolve(results);};});}};exports.AppsMetadata=AppsMetadata;}(window));;'use strict';(function(exports){function Datastore(name){this.name=name;}
-Datastore.prototype={name:'',DB_VERSION:1,datastore:null,lastRevision:null,db:null,get revisionName(){return this.name+'_revision';},get idbName(){return this.name+'_mirror';},get storeName(){return this.name+'_store';},init:function(){var revisionString=localStorage.getItem(this.revisionName);if(revisionString){this.lastRevision=JSON.parse(revisionString);}
+Datastore.prototype={name:'',DB_VERSION:1,datastore:null,lastRevision:null,db:null,get revisionName(){return this.name+'_revision';},get idbName(){return this.name+'_mirror';},get storeName(){return this.name+'_store';},init:function(){var revisionString;try{revisionString=localStorage.getItem(this.revisionName);}catch(e){console.error('Homescreen datastore revision unavailable:',e);}if(revisionString){this.lastRevision=JSON.parse(revisionString);}
return new Promise((resolve,reject)=>{var req=window.indexedDB.open(this.idbName,this.DB_VERSION);req.onupgradeneeded=this.upgradeSchema.bind(this);req.onsuccess=(e)=>{this.db=e.target.result;resolve();if(!navigator.getDataStores){console.error('DataStore API is unavailable');return;}
navigator.getDataStores(this.name).then((stores)=>{if(stores.length<1){console.error(this.name+' inaccessible');return;}
this.datastore=stores[0];this.datastore.addEventListener('change',this.onChange.bind(this));this.synchronise();},(e)=>{console.error('Error getting datastore',e);});};req.onerror=(e)=>{console.error('Error opening datastore mirror database',e);reject(e);};});},upgradeSchema:function(e){var db=e.target.result;var fromVersion=e.oldVersion;if(fromVersion<1){var store=db.createObjectStore(this.storeName,{keyPath:'id'});store.createIndex('data','data',{unique:false});}},synchronise:function(){this._syncQueue=(this._syncQueue||Promise.resolve()).then(()=>this._synchronise());return this._syncQueue;},_synchronise:function(){return new Promise((resolve,reject)=>{var cursor=this.datastore.sync(this.lastRevision);var self=this;function cursorResolve(task){var promises=[];switch(task.operation){case'update':case'add':promises.push(self.set(task.data));break;case'remove':promises.push(self.remove(task.id));break;case'clear':promises.push(self.clear());break;case'done':self.updateRevision();resolve();return;}
promises.push(cursor.next());Promise.all(promises).then((results)=>{cursorResolve(results.pop());},reject);}
-cursor.next().then(cursorResolve,reject);});},set:function(data){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(data,resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-set',{detail:{id:data.id}}));}.bind(this,data,resolve);txn.onerror=reject;try{txn.objectStore(this.storeName).put({id:data.id,data:data});}catch(e){console.error('Error putting data in '+this.idbName+':',e);resolve();}});},remove:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(id,resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-removed',{detail:{id:id}}));}.bind(this,id,resolve);txn.onerror=reject;try{txn.objectStore(this.storeName).delete(id);}catch(e){console.error('Error deleting data from '+this.idbName+':',e);resolve();}});},clear:function(){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-cleared'));}.bind(this,resolve);txn.onerror=reject;txn.objectStore(this.storeName).clear();});},updateRevision:function(){this.lastRevision=this.datastore.revisionId;localStorage.setItem(this.revisionName,JSON.stringify(this.lastRevision));},onChange:function(e){this.synchronise().then(()=>{},(error)=>{console.error('Failed to handle '+this.name+' change',error);});},get:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readonly');txn.onerror=reject;txn.objectStore(this.storeName).get(id).onsuccess=(event)=>{resolve(event.target.result);};});},getAll:function(filter=()=>true){return new Promise((resolve,reject)=>{var results=[];var txn=this.db.transaction([this.storeName],'readonly');txn.onerror=reject;txn.oncomplete=()=>{resolve(results);};txn.objectStore(this.storeName).openCursor().onsuccess=(event)=>{var cursor=event.target.result;if(cursor){if(filter(cursor.value)){results.push(cursor.value);}
+cursor.next().then(cursorResolve,reject);});},set:function(data){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(data,resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-set',{detail:{id:data.id}}));}.bind(this,data,resolve);txn.onerror=reject;try{txn.objectStore(this.storeName).put({id:data.id,data:data});}catch(e){console.error('Error putting data in '+this.idbName+':',e);resolve();}});},remove:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(id,resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-removed',{detail:{id:id}}));}.bind(this,id,resolve);txn.onerror=reject;try{txn.objectStore(this.storeName).delete(id);}catch(e){console.error('Error deleting data from '+this.idbName+':',e);resolve();}});},clear:function(){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-cleared'));}.bind(this,resolve);txn.onerror=reject;txn.objectStore(this.storeName).clear();});},updateRevision:function(){this.lastRevision=this.datastore.revisionId;try{localStorage.setItem(this.revisionName,JSON.stringify(this.lastRevision));}catch(e){console.error('Homescreen datastore revision could not be saved:',e);}},onChange:function(e){this.synchronise().then(()=>{},(error)=>{console.error('Failed to handle '+this.name+' change',error);});},get:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readonly');txn.onerror=reject;txn.objectStore(this.storeName).get(id).onsuccess=(event)=>{resolve(event.target.result);};});},getAll:function(filter=()=>true){return new Promise((resolve,reject)=>{var results=[];var txn=this.db.transaction([this.storeName],'readonly');txn.onerror=reject;txn.oncomplete=()=>{resolve(results);};txn.objectStore(this.storeName).openCursor().onsuccess=(event)=>{var cursor=event.target.result;if(cursor){if(filter(cursor.value)){results.push(cursor.value);}
cursor.continue();}};});}};exports.Datastore=Datastore;}(window));;'use strict';(function(exports){function PagesStore(name){this.name=name;}
PagesStore.prototype=Object.create(Datastore.prototype);PagesStore.prototype.DB_VERSION=1;PagesStore.prototype.set=function(data){data.id=data.url;return Datastore.prototype.set.call(this,data);};PagesStore.prototype.getAll=function(){var filter=(value)=>value.data&&value.data.pinned;return Datastore.prototype.getAll.call(this,filter);};exports.PagesStore=PagesStore;}(window));;'use strict';(function(exports){const PAGES_ICON_SIZE=30;function Pages(){this.panel=document.getElementById('pages-panel');this.panels=document.getElementById('panels');this.pages=document.getElementById('pages');this.scrollable=document.querySelector('#pages-panel > .scrollable');this.bottombar=document.getElementById('bottombar');this.remove=document.getElementById('remove');this.done=document.getElementById('done');this.empty=true;this.editMode=false;this.dialogs=[];this.pages.addEventListener('click',this);this.pages.addEventListener('contextmenu',this);this.pages.addEventListener('keydown',this);window.addEventListener('resize',this);this.done.addEventListener('click',e=>{e.preventDefault();this.exitEditMode();});this.remove.addEventListener('click',e=>{e.preventDefault();this.unpinSelectedCard();});this.pagesStore=new PagesStore('places');this.pagesStore.init().then(()=>{document.addEventListener('places-set',(e)=>{var id=e.detail.id;this.pagesStore.get(id).then((page)=>{for(var child of this.pages.children){if(child.dataset.id===id){if(!page.data.pinned){this.removeCard(child);}else{this.updatePinnedPage(child,page.data);}
return;}}
diff --git a/assets/webapps/homescreen.gaiamobile.org/js/datastore.js b/assets/webapps/homescreen.gaiamobile.org/js/datastore.js
index 8d487016..3994de4f 100644
--- a/assets/webapps/homescreen.gaiamobile.org/js/datastore.js
+++ b/assets/webapps/homescreen.gaiamobile.org/js/datastore.js
@@ -1,8 +1,8 @@
;'use strict';(function(exports){function Datastore(name){this.name=name;}
-Datastore.prototype={name:'',DB_VERSION:1,datastore:null,lastRevision:null,db:null,get revisionName(){return this.name+'_revision';},get idbName(){return this.name+'_mirror';},get storeName(){return this.name+'_store';},init:function(){var revisionString=localStorage.getItem(this.revisionName);if(revisionString){this.lastRevision=JSON.parse(revisionString);}
+Datastore.prototype={name:'',DB_VERSION:1,datastore:null,lastRevision:null,db:null,get revisionName(){return this.name+'_revision';},get idbName(){return this.name+'_mirror';},get storeName(){return this.name+'_store';},init:function(){var revisionString;try{revisionString=localStorage.getItem(this.revisionName);}catch(e){console.error('Homescreen datastore revision unavailable:',e);}if(revisionString){this.lastRevision=JSON.parse(revisionString);}
return new Promise((resolve,reject)=>{var req=window.indexedDB.open(this.idbName,this.DB_VERSION);req.onupgradeneeded=this.upgradeSchema.bind(this);req.onsuccess=(e)=>{this.db=e.target.result;resolve();if(!navigator.getDataStores){console.error('DataStore API is unavailable');return;}
navigator.getDataStores(this.name).then((stores)=>{if(stores.length<1){console.error(this.name+' inaccessible');return;}
this.datastore=stores[0];this.datastore.addEventListener('change',this.onChange.bind(this));this.synchronise();},(e)=>{console.error('Error getting datastore',e);});};req.onerror=(e)=>{console.error('Error opening datastore mirror database',e);reject(e);};});},upgradeSchema:function(e){var db=e.target.result;var fromVersion=e.oldVersion;if(fromVersion<1){var store=db.createObjectStore(this.storeName,{keyPath:'id'});store.createIndex('data','data',{unique:false});}},synchronise:function(){this._syncQueue=(this._syncQueue||Promise.resolve()).then(()=>this._synchronise());return this._syncQueue;},_synchronise:function(){return new Promise((resolve,reject)=>{var cursor=this.datastore.sync(this.lastRevision);var self=this;function cursorResolve(task){var promises=[];switch(task.operation){case'update':case'add':promises.push(self.set(task.data));break;case'remove':promises.push(self.remove(task.id));break;case'clear':promises.push(self.clear());break;case'done':self.updateRevision();resolve();return;}
promises.push(cursor.next());Promise.all(promises).then((results)=>{cursorResolve(results.pop());},reject);}
-cursor.next().then(cursorResolve,reject);});},set:function(data){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(data,resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-set',{detail:{id:data.id}}));}.bind(this,data,resolve);txn.onerror=reject;try{txn.objectStore(this.storeName).put({id:data.id,data:data});}catch(e){console.error('Error putting data in '+this.idbName+':',e);resolve();}});},remove:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(id,resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-removed',{detail:{id:id}}));}.bind(this,id,resolve);txn.onerror=reject;try{txn.objectStore(this.storeName).delete(id);}catch(e){console.error('Error deleting data from '+this.idbName+':',e);resolve();}});},clear:function(){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-cleared'));}.bind(this,resolve);txn.onerror=reject;txn.objectStore(this.storeName).clear();});},updateRevision:function(){this.lastRevision=this.datastore.revisionId;localStorage.setItem(this.revisionName,JSON.stringify(this.lastRevision));},onChange:function(e){this.synchronise().then(()=>{},(error)=>{console.error('Failed to handle '+this.name+' change',error);});},get:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readonly');txn.onerror=reject;txn.objectStore(this.storeName).get(id).onsuccess=(event)=>{resolve(event.target.result);};});},getAll:function(filter=()=>true){return new Promise((resolve,reject)=>{var results=[];var txn=this.db.transaction([this.storeName],'readonly');txn.onerror=reject;txn.oncomplete=()=>{resolve(results);};txn.objectStore(this.storeName).openCursor().onsuccess=(event)=>{var cursor=event.target.result;if(cursor){if(filter(cursor.value)){results.push(cursor.value);}
+cursor.next().then(cursorResolve,reject);});},set:function(data){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(data,resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-set',{detail:{id:data.id}}));}.bind(this,data,resolve);txn.onerror=reject;try{txn.objectStore(this.storeName).put({id:data.id,data:data});}catch(e){console.error('Error putting data in '+this.idbName+':',e);resolve();}});},remove:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(id,resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-removed',{detail:{id:id}}));}.bind(this,id,resolve);txn.onerror=reject;try{txn.objectStore(this.storeName).delete(id);}catch(e){console.error('Error deleting data from '+this.idbName+':',e);resolve();}});},clear:function(){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-cleared'));}.bind(this,resolve);txn.onerror=reject;txn.objectStore(this.storeName).clear();});},updateRevision:function(){this.lastRevision=this.datastore.revisionId;try{localStorage.setItem(this.revisionName,JSON.stringify(this.lastRevision));}catch(e){console.error('Homescreen datastore revision could not be saved:',e);}},onChange:function(e){this.synchronise().then(()=>{},(error)=>{console.error('Failed to handle '+this.name+' change',error);});},get:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readonly');txn.onerror=reject;txn.objectStore(this.storeName).get(id).onsuccess=(event)=>{resolve(event.target.result);};});},getAll:function(filter=()=>true){return new Promise((resolve,reject)=>{var results=[];var txn=this.db.transaction([this.storeName],'readonly');txn.onerror=reject;txn.oncomplete=()=>{resolve(results);};txn.objectStore(this.storeName).openCursor().onsuccess=(event)=>{var cursor=event.target.result;if(cursor){if(filter(cursor.value)){results.push(cursor.value);}
cursor.continue();}};});}};exports.Datastore=Datastore;}(window));
\ No newline at end of file
diff --git a/assets/webapps/homescreen.gaiamobile.org/js/settings.js b/assets/webapps/homescreen.gaiamobile.org/js/settings.js
index 649e6374..c88e7f9a 100644
--- a/assets/webapps/homescreen.gaiamobile.org/js/settings.js
+++ b/assets/webapps/homescreen.gaiamobile.org/js/settings.js
@@ -1,6 +1,6 @@
;'use strict';(function(exports){const SETTINGS_STORE='homescreen_settings';const SETTINGS_VERSION=0;const COLUMNS_SETTING='grid.cols';const PAGING_SETTING='grid.paging';function Settings(){this.small=false;this.scrollSnapping=false;this.firstRun=false;if(navigator.getDataStores){navigator.getDataStores(SETTINGS_STORE).then(stores=>{if(stores.length<1){return Promise.reject('Settings datastore inaccessible');}
var signalChange=()=>{this.save();window.dispatchEvent(new CustomEvent('settings-changed'));};var syncSmallSetting=(signal)=>{stores[0].get(COLUMNS_SETTING).then(cols=>{var oldSmall=this.small;this.small=cols>3;if(this.small!==oldSmall){signalChange();}});};var syncPagingSetting=()=>{stores[0].get(PAGING_SETTING).then(paging=>{var oldSnapping=this.scrollSnapping;this.scrollSnapping=paging||false;if(this.scrollSnapping!==oldSnapping){signalChange();}});};stores[0].addEventListener('change',e=>{switch(e.id){case COLUMNS_SETTING:syncSmallSetting();break;case PAGING_SETTING:syncPagingSetting();break;}});syncSmallSetting();syncPagingSetting();},e=>{console.error('Error retrieving home screen settings datastore:',e);});}else{console.error('Datastore API unavailable');}
-var settingsString=localStorage.getItem('settings');if(!settingsString){this.firstRun=true;return;}
+var settingsString;try{settingsString=localStorage.getItem('settings');}catch(e){console.error('Home screen local settings unavailable:',e);return;}if(!settingsString){this.firstRun=true;return;}
var settings=JSON.parse(settingsString);if(settings.version!==SETTINGS_VERSION){return;}
this.small=settings.small||false;this.scrollSnapping=settings.scrollSnapping||false;if(!navigator.getDataStores){console.error('Datastore API unavailable');return;}}
-Settings.prototype={save:function(){localStorage.setItem('settings',JSON.stringify({version:SETTINGS_VERSION,small:this.small,scrollSnapping:this.scrollSnapping}));}};exports.Settings=Settings;}(window));
\ No newline at end of file
+Settings.prototype={save:function(){try{localStorage.setItem('settings',JSON.stringify({version:SETTINGS_VERSION,small:this.small,scrollSnapping:this.scrollSnapping}));}catch(e){console.error('Home screen settings could not be saved:',e);}}};exports.Settings=Settings;}(window));
\ No newline at end of file
diff --git a/assets/webapps/marketplace.gaiamobile.org/marketplace.js b/assets/webapps/marketplace.gaiamobile.org/marketplace.js
index b00ac828..bb7eee4f 100644
--- a/assets/webapps/marketplace.gaiamobile.org/marketplace.js
+++ b/assets/webapps/marketplace.gaiamobile.org/marketplace.js
@@ -12,7 +12,8 @@
'The catalogue is closed. You can reopen it below.'));
document.getElementById('open').hidden=true;
});
- function browse(){labels();message.hidden=true;dialog.hidden=true;frame.style.visibility='visible';frame.src='https://vulpes-os.org/marketplace/?mode=app&lang='+(fr()?'fr':'en');}
+ let catalogueUrl='';
+ function browse(reload=false){labels();message.hidden=true;dialog.hidden=true;frame.style.visibility='visible';const next='https://vulpes-os.org/marketplace/?mode=app&lang='+(fr()?'fr':'en');if(reload||!catalogueUrl){catalogueUrl=next;frame.src=next;}}
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;
@@ -22,7 +23,10 @@
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;}
+ }catch(error){if(current!==attempt)return;status(error.message==='PRIVILEGED_APP_UNSUPPORTED'?
+ text('Cette application demande des API privilégiées qui ne sont pas disponibles dans Vulpes OS.',
+ 'This app requires privileged APIs that are not available in Vulpes OS.'):
+ 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;}
@@ -32,7 +36,7 @@
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();});
+ addEventListener('languagechange',()=>{language=navigator.language;if(!busy)browse(true);});
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/search.gaiamobile.org/manifest.webapp b/assets/webapps/search.gaiamobile.org/manifest.webapp
index f67bea8e..7f374c4c 100755
--- a/assets/webapps/search.gaiamobile.org/manifest.webapp
+++ b/assets/webapps/search.gaiamobile.org/manifest.webapp
@@ -10,152 +10,20 @@
},
"locales": {
"ar": {
- "name": "مُتصفّح",
- "description": "مُتصفّح نظام Gaia"
- },
- "bg": {
- "name": "Браузър",
- "description": "Системен уеб браузър на Gaia"
- },
- "bn-BD": {
- "name": "ব্রাউজার",
- "description": "গায়া সিস্টেম ওয়েব ব্রাউজার"
- },
- "ca": {
- "name": "Navegador",
- "description": "Navegador web del sistema del Gaia"
- },
- "cs": {
- "name": "Prohlížeč",
- "description": "Vyhledávání"
- },
- "da": {
"name": "Browser",
- "description": "Gaia system-webbrowser"
- },
- "de": {
- "name": "Browser",
- "description": "System-Webbrowser für Gaia"
- },
- "el": {
- "name": "Περιηγητής",
- "description": "Περιηγητής Ιστού του Gaia"
+ "description": "Gaia System Web Browser"
},
"en-US": {
"name": "Browser",
"description": "Gaia System Web Browser"
},
- "es": {
- "name": "Navegador",
- "description": "Navegador del sistema Gaia"
- },
- "eu": {
- "name": "Nabigatzailea",
- "description": "Gaia, sistemaren web nagibatzailea"
- },
"fr": {
"name": "Navigateur",
"description": "Navigateur web système de Gaia"
},
- "gl": {
- "name": "Navegador",
- "description": "Navegador web do sistema Gaia"
- },
- "hr": {
+ "zh-TW": {
"name": "Browser",
"description": "Gaia System Web Browser"
- },
- "hu": {
- "name": "Böngésző",
- "description": "Gaia webbögésző"
- },
- "it": {
- "name": "Browser",
- "description": "Browser di sistema di Gaia"
- },
- "ja": {
- "name": "ブラウザ",
- "description": "Gaia システムの Web ブラウザ"
- },
- "km": {
- "name": "Browser",
- "description": "កម្មវិធីអ៊ីនធឺណិតជាប្រព័ន្ធ Gaia"
- },
- "ko": {
- "name": "브라우저",
- "description": "가이아 시스템 웹 브라우저"
- },
- "lt": {
- "name": "Naršyklė",
- "description": "„Gaia“ saityno naršyklė"
- },
- "mk": {
- "name": "Пребарување",
- "description": "Резултати од пребарување"
- },
- "ms": {
- "name": "Pelayar",
- "description": "Pelayar Web Sistem Gaia"
- },
- "ne-NP": {
- "name": "ब्राउजर",
- "description": "गैया प्रणाली वेब ब्राउजर"
- },
- "nl": {
- "name": "Browser",
- "description": "Gaia-systeemwebbrowser"
- },
- "pa": {
- "name": "ਖੋਜ ਨਤੀਜੇ",
- "description": "ਖੋਜ ਨਤੀਜੇ"
- },
- "pl": {
- "name": "Firefox",
- "description": "Systemowa przeglądarka"
- },
- "pt-BR": {
- "name": "Navegador",
- "description": "Navegador do Gaia"
- },
- "ro": {
- "name": "Browser",
- "description": "Browser web în sistemul Gaia"
- },
- "ru": {
- "name": "Браузер",
- "description": "Системный веб-браузер Gaia"
- },
- "sk": {
- "name": "Prehliadač",
- "description": "Webový prehliadač Gaia"
- },
- "sq": {
- "name": "Shfletues",
- "description": "Shfletuesi Web i Sistemit Gaia"
- },
- "sr-Cyrl": {
- "name": "Прегледач",
- "description": "Gaia веб прегледач"
- },
- "sr-Latn": {
- "name": "Pregledač",
- "description": "Gaia veb pregledač"
- },
- "sv-SE": {
- "name": "Webbläsare",
- "description": "Gaia systemwebbläsare"
- },
- "tr": {
- "name": "Tarayıcı",
- "description": "Gaia Sistem Web Tarayıcısı"
- },
- "zh-CN": {
- "name": "浏览器",
- "description": "Gaga 系统的网页浏览器"
- },
- "zh-TW": {
- "name": "瀏覽器",
- "description": "Gaia 系統網頁瀏覽器"
}
},
"default_locale": "en-US",
@@ -191,7 +59,12 @@
}
},
"icons": {
- "284": "/shared/resources/branding/vulpes-browser.png"
+ "84": "/shared/resources/branding/browser_84.png",
+ "126": "/shared/resources/branding/browser_126.png",
+ "142": "/shared/resources/branding/browser_142.png",
+ "189": "/shared/resources/branding/browser_189.png",
+ "256": "/shared/resources/branding/browser_256.png",
+ "284": "/shared/resources/branding/browser_284.png"
},
"chrome": {
"navigation": true
@@ -207,7 +80,5 @@
"disposition": "window",
"href": "/index.html"
}
- },
- "origin": "app://search.gaiamobile.org",
- "version": "2.5"
+ }
}
diff --git a/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_126.png b/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_126.png
index eef767da..905faf81 100644
Binary files a/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_126.png and b/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_126.png differ
diff --git a/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_142.png b/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_142.png
index eef767da..7a3346bf 100644
Binary files a/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_142.png and b/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_142.png differ
diff --git a/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_189.png b/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_189.png
index eef767da..7bfb5074 100644
Binary files a/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_189.png and b/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_189.png differ
diff --git a/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_256.png b/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_256.png
new file mode 100644
index 00000000..f665bfe1
Binary files /dev/null and b/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_256.png differ
diff --git a/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_284.png b/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_284.png
index eef767da..21913992 100644
Binary files a/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_284.png and b/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_284.png differ
diff --git a/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_336.png b/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_336.png
new file mode 100644
index 00000000..3e15ea49
Binary files /dev/null and b/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_336.png differ
diff --git a/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_84.png b/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_84.png
index eef767da..5089aa37 100644
Binary files a/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_84.png and b/assets/webapps/search.gaiamobile.org/shared/resources/branding/browser_84.png differ
diff --git a/assets/webapps/settings.gaiamobile.org/locales-obj/index.en-US.json b/assets/webapps/settings.gaiamobile.org/locales-obj/index.en-US.json
index 18653dc9..6f91991e 100644
--- a/assets/webapps/settings.gaiamobile.org/locales-obj/index.en-US.json
+++ b/assets/webapps/settings.gaiamobile.org/locales-obj/index.en-US.json
@@ -1 +1 @@
-{"gaia-menu-cancel":"Cancel","internetSharing-usb-description":"Share my Internet connection with a USB-connected device.","internetSharing-wifi-description":"Share my Internet connection with Wi-Fi-connected devices.","unplayable-ringtone":"The ringtone was not changed because the selected file can not be played on this phone.","findmydevice-signup-description":"Find My Device allows you to locate, lock or erase your phone","findmydevice-settings-description":"Find My Device allows you to locate, lock or erase your phone from a web site:","findmydevice-active-tracking":"Your phone is being tracked by another device through the website.","findmydevice-not-tracking":"No devices are actively tracking your phone through the website.","ums-confirm":"Enabling USB storage allows another computer or device connected using USB to access files on this phone.","reset-device":"Reset Phone","reset-warning-body":"Resetting will erase all your data, including any apps you\u2019ve purchased. The phone will be restored to its factory condition. None of your settings or data will be saved.","reset-devtools-warning-body":"Resetting will erase all your data, including any apps you\u2019ve purchased. None of your settings or data will be saved. But your phone will have unrestricted developer tools, allowing you to modify and debug system applications.","unreset-devtools-warning-body":"Resetting will erase all your data, including any apps you\u2019ve purchased. None of your settings or data will be saved. Your phone will no longer have unrestricted developer tools. You will no longer be able to to modify or debug system applications.","powerSave-explanation":"Turning on power save mode turns off the phone\u2019s data, Bluetooth and Geolocation connections to extend battery life. You can still turn these services back on manually.","performanceAndUsageDataInfo":{"value":["Help us improve ",{"type":"idOrVar","name":"brandShortName"}," by sharing data about your phone and the way you use it."]},"device-status-connected-device":"Connected to phone audio","device-status-connected-device-media":"Connected to phone/media audio","settings":"Settings","advancedSettings":"Advanced Settings","default":"Default","enabled":"Enabled","enable":"Enable","disabled":"Disabled","error":"Error","ok":"OK","yes":"Yes","no":"No","reset":"Reset","factory-reset":"Factory Reset","delete":"Delete","cancel":"Cancel","close":"Close","back":"Back","continue":"Continue","skip":"Skip","pair":"Pair","add":"Add","done":"Done","set":"Set","notNow":"Not now","turnOn":"Turn ON","selectASIMCard":"Select a SIM card","sim1":"SIM 1","sim2":"SIM 2","unavailable":"Not available","confirmation":"Confirmation","simSettingsWithIndex":{"value":["SIM ",{"type":"idOrVar","name":"index"}," Settings"]},"simWithIndex":{"value":["SIM ",{"type":"idOrVar","name":"index"}]},"networkAndConnectivity":"Network & Connectivity","airplaneMode":{"value":"Airplane Mode","attrs":{"ariaLabel":"Airplane Mode"}},"geolocation":{"value":"Geolocation","attrs":{"ariaLabel":"Geolocation"}},"wifi":"Wi-Fi","wifi-header":{"value":[{"type":"idOrVar","name":"wifi"}]},"wifi-disabled":"Turn Wi-Fi on to view available networks.","availableNetworks":"Available Networks","knownNetworks":"Known Networks","hiddenNetworks":"Hidden Networks","scanning":"Searching\u2026","scanNetworks":"Search Again","full-status-initializing":"Initializing\u2026","full-status-connecting":{"value":["Connecting to ",{"type":"idOrVar","name":"ssid"},"\u2026"]},"full-status-associated":"Obtaining an IP address\u2026","full-status-connected":{"value":["Connected to ",{"type":"idOrVar","name":"ssid"}]},"full-status-connectingfailed":"Connection failed","full-status-disconnected":"Not connected","short-status-connecting":"Connecting\u2026","short-status-associated":"Obtaining an IP address\u2026","short-status-connected":"Connected","short-status-connectingfailed":"Connection failed","short-status-disconnected":"Not connected","wifi-sleep":"Wi-Fi Sleep","wifi-sleep-description":"Enable to automatically turn off Wi-Fi during sleep to save battery power.","manageNetworks":"Manage Networks","wifi-bad-credentials-title":"Failed connecting to Wi-Fi","wifi-bad-credentials-confirm":{"value":["Unable to connect to ",{"type":"idOrVar","name":"ssid"},", authentication failed due to bad credentials. Please re-enter the network password."]},"manageNetworks-header":{"value":[{"type":"idOrVar","name":"manageNetworks"}]},"manageCertificates":"Manage Certificates","manageCertificates-header":{"value":[{"type":"idOrVar","name":"manageCertificates"}]},"joinHiddenNetwork":"Join Hidden Network","securedBy":{"value":["Secured by ",{"type":"idOrVar","name":"capabilities"}]},"security":"Security","securityNone":"None","securityOpen":"Open","notInRange":"Not in range","ipAddress":"IP address","linkSpeed":"Link speed","linkSpeedMbs":{"value":[{"type":"idOrVar","name":"linkSpeed"}," Mbit/s"]},"signalStrength":"Signal strength","signalLevel0":"Very weak","signalLevel1":"Weak","signalLevel2":"Average","signalLevel3":"Good","signalLevel4":"Very good","forget":"Forget","forgetNetwork-dialog":"Forget network?","forgetNetwork-confirmation":"Confirmation","authentication":"Authentication","authentication-header":{"value":[{"type":"idOrVar","name":"authentication"}]},"eap":"EAP method","auth-phase2":"Phase 2 authentication","server-certificate":"Server certificate","import-certificate-msg":"Notice: If you need to import new certificates, please go to \"Manage certificates\".","identity":"Identifier","password":"Password","showPassword":"Show password","networkNotification":"Network notification","networkNotification-expl":"Notify me when an open network is available","macAddress":"MAC Address","deviceInfo_software":{"value":[{"type":"idOrVar","name":"brandShortName"}," ",{"type":"idOrVar","name":"os"}]},"macUnavailable":"Not available","noNetworksFound":"No networks found","noKnownNetworks":"No known networks","edit":"Edit","certificateList":"Certificate List","noImportedCertificates":"No certificates imported","deleteCertificate":"Delete Certificate","importCertificate":"Import Certificate","selectCertificateFile":"Select Certificate File","enterCertificateNickname":"Enter certificate nick name","nickname":"Nick name","enter-certificate-name-msg":"You can change the nick name or keep the default name.","certificate-deletion-failed":"Certificate deletion failed","certificate-deletion-failed-description":"This may be due to a storage issue, for example.","certificate-import-failed":"Certificate import failed","certificate-import-failed-description":"This may be due to a damaged certificate file, for example.","wpsMessage":"Connect with WPS","wpsMessage-header":{"value":[{"type":"idOrVar","name":"wpsMessage"}]},"fullStatus-wps-inprogress":"WPS is in progress\u2026","fullStatus-wps-canceled":"WPS was canceled","fullStatus-wps-timedout":"WPS timed out","fullStatus-wps-failed-error":{"value":["WPS failed [",{"type":"idOrVar","name":"error"},"]"]},"fullStatus-wps-overlapped":"Another WPS PBC AP was found","wpsDescription2":"Automatic Wi-Fi setup","wpsMethodSelection":"Select a WPS Method:","wpsPbcLabel":"Button connection","wpsMyPinLabel":"My PIN connection","wpsApPinLabel":"AP PIN connection","wpsPinDescription":"PIN is 4 or 8 digits","wpsPinInput":{"value":["Input ",{"type":"idOrVar","name":"pin"}," to an opposite device"]},"wpsPinAps":"Select an opposite device","wpsAnyAp":"Any","wpsCancelMessage":"Cancel WPS","wpsCancelFailedMessageError":{"value":["Failed to cancel WPS [",{"type":"idOrVar","name":"error"},"]"]},"callSettings":"Call Settings","callSettings-header":{"value":[{"type":"idOrVar","name":"callSettings"}]},"callSettingsQuery":"Requesting network info\u2026","callSettingsQueryError":"Error in query","voiceMail":"Voicemail","voiceMail-header":{"value":[{"type":"idOrVar","name":"voiceMail"}]},"voiceMail-number":"Voicemail Number","voiceMail-number-notSet":"Not set","voicePrivacyMode":"Voice Privacy Mode","callWaiting":"Call Waiting","confirmCallWaitingTitle":"Unable to confirm call waiting preferences","confirmCallWaitingDesc":"The device is currently unable to communicate with the carrier. Try setting the preference again.","callForwarding":"Call Forwarding","callForwarding-header":{"value":[{"type":"idOrVar","name":"callForwarding"}]},"callForwardingForward":"Forward","callForwardingUnconditional":"Always Forward","callForwardingUnconditionalShort":"Always","callForwardingMobileBusy":"Forward When Busy","callForwardingMobileBusyShort":"When busy","callForwardingNoReply":"Forward When Unanswered","callForwardingNoReplyShort":"When unanswered","callForwardingNotReachable":"Forward When Unreachable","callForwardingNotReachableShort":"When unreachable","callForwardingForwardingVoiceToNumber":{"value":["Voice calls to ",{"type":"idOrVar","name":"number"},""]},"callForwardingNotForwarding":"Disabled","callForwardingConfirmTitle":"Call forwarding confirmation message","cf-alwaysUseThisNumber":"Always use this number","cf-numberWhenUnreachable":"Number when unreachable","cf-numberWhenUnanswered":"Number when unanswered","cf-numberWhenBusy":"Number when busy","callForwardingInvalidNumberError":"This phone number is invalid.","callForwardingSetSuccess":"Operation successfully set.","callForwardingSetForbidden":"Your operator does not support that operation.","callForwardingSetError":"Call settings error.","callerId":"Caller ID","callBarring":"Call Barring","callBarring-header":{"value":[{"type":"idOrVar","name":"callBarring"}]},"callBarring-outgoing":"Outgoing Calls","callBarring-baoc":"All","callBarring-boic":"International","callBarring-boicExhc":"International Except Home Country","callBarring-incoming":"Incoming Calls","callBarring-baic":"All","callBarring-baicR":"All While Roaming","callBarring-passcode":"Passcode","callBarring-change-passcode":"Change passcode","callBarring-passcode-enter":"Enter passcode","callBarring-passcode-unknown":"In case you don't know your passcode, please contact your carrier","callBarring-current-passcode":"Current passcode","callBarring-new-passcode":"New passcode","callBarring-create-passcode":"Create a Passcode","callBarring-confirm-passcode":"Confirm passcode","callBarring-passcode-error-generic":"Passcode error.","callBarring-passcode-error-incorrect":"Passcode is incorrect.","callBarring-passcode-error-mismatch":"Passcode doesn\u2019t match.","callBarring-update-item-error":{"value":["Error while updating the item: ",{"type":"idOrVar","name":"error"},"."]},"callBarring-update-generic-error":"Network or passcode error","callBarring-change-passcode-success":"Passcode changed.","callerId-default":"Network default","callerId-hide":"Hide number","callerId-show":"Show number","fdn":"Fixed dialing numbers","fdnSettings":"Fixed Dialing Numbers","fdnSettings-header":{"value":[{"type":"idOrVar","name":"fdnSettings"}]},"fdn-authorizedNumbers":"Authorized Numbers","fdn-authorizedNumbers-header":{"value":[{"type":"idOrVar","name":"fdn-authorizedNumbers"}]},"fdn-authorizedNumbers-desc":"List of numbers that can be called","fdn-explanation1":"Restrict which numbers can be dialed from this phone.","fdn-explanation2":"(Emergency numbers will always be dialable)","fdnEnable":"Enable FDN","fdnDisable":"Disable FDN","fdnReset":"Reset PIN2","fdnAttemptMsg3":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"one":"You have one try left to enter the correct code before locking the SIM card.","two":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before locking the SIM card."],"few":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before locking the SIM card."],"many":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before locking the SIM card."],"other":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before locking the SIM card."]}},"fdnLastChanceMsg":"This is your last chance to enter the correct PIN2. Otherwise, you must enter the PUK2 code to use this SIM card.","fdnErrorMsg":"The PIN2 code is incorrect.","fdnContact-name":"name","fdnContact-number":"number","fdnContact-submit":"submit","fdnAction-add":"Add contact","fdnAction-call":"Call","fdnAction-edit":"Edit contact","fdnAction-edit-header":"Edit contact","fdnAction-delete":"Delete contact","fdnNoFDNFreeRecord":"Maximum FDN reached. You cannot add more numbers.","messagingSettings":"Messaging Settings","messagingSettings-header":{"value":[{"type":"idOrVar","name":"messagingSettings"}]},"message-confirmation":"Message Confirmation","message-delivery-reports":"Delivery Reports","message-delivery-reports-details":"Request notification of delivery for each message sent","message-read-reports":"Read Reports","message-read-reports-details":"Request read report for each message sent","mmsTitle":"Multimedia Messaging (MMS)","auto-retrieve":"Auto Retrieve","auto-retrieve-details":"Automatically retrieve messages","manual-retrieve":"Off","auto-roaming-retrieve":"On with roaming","auto-no-roaming-retrieve":"On without roaming","autoConfigure":"Auto-configure","wapPushMessages":"WAP Push Messages","wapPush":"WAP Push","cellBroadcastMessages":"Cell Broadcast Messages","cellBroadcast":"Cell Broadcast","emergencyAlert":"Emergency Alert","messagingInformation":"Information","messaging-SMSC":"Service Center","messaging-sim-settings":{"value":["SIM ",{"type":"idOrVar","name":"index"}," Settings"]},"unknown-SMSC":"Info not available","cellularAndData":"Cellular & Data","cellularAndData-header":{"value":[{"type":"idOrVar","name":"cellularAndData"}]},"dataOperator":"Operator","dataConnectivity":"Data","dataConnection":"Data Connection","dataRoaming":"Data Roaming","netOperator":"Operator","netOperator-header":{"value":[{"type":"idOrVar","name":"netOperator"}]},"availableOp":"Operators in the Area","preferredNetworkTypeAlertTitle":"Preferred network type","preferredNetworkTypeAlertErrorMessage":"Network type could not be set","operator-networkType":"Network Type","operator-networkType-auto":"Automatic","operator-networkType-prefer3G":"3G preferred","operator-networkType-2G":"2G only","operator-networkType-3G":"3G only","operator-networkType-GSM":"GSM","operator-networkType-WCDMA":"WCDMA","operator-networkType-CDMA":"CDMA","operator-networkType-EVDO":"EVDO","operator-networkType-LTE":"LTE","operator-networkType-preferWCDMA":"WCDMA preferred","operator-networkType-auto-2G-3G":"Auto (2G/3G)","operator-networkType-auto-3G-4G":"Auto (3G/4G)","operator-networkType-auto-CDMA-EVDO":"Auto (CDMA/EVDO)","operator-networkType-auto-WCDMA-GSM":"Auto (WCDMA/GSM)","operator-networkType-auto-LTE-WCDMA-GSM":"Auto (LTE/WCDMA/GSM)","operator-networkType-auto-LTE-WCDMA":"Auto (LTE/WCDMA)","operator-networkType-auto-LTE-CDMA-EVDO":"Auto (LTE/CDMA/EVDO)","operator-networkType-auto-WCDMA-GSM-CDMA-EVDO":"Auto (WCDMA/GSM/CDMA/EVDO)","operator-autoSelect":"Automatic Selection","operator-networkSelect-auto":"Automatic","operator-networkSelect-manual":"Manual","operator-turnAutoSelectionOff":"Turn automatic selection off to view available operators.","operator-info-connecting":"Connecting\u2026","operator-info-failed":"Connection failed","operator-info-connected":"Connected","operator-info-available":"Available","operator-info-current":"Connected","operator-info-forbidden":"Forbidden","operator-info-unknown":"Unknown state","operator-roamingPreference":"Roaming preference","operator-roamingPreference-home":"Home","operator-roamingPreference-any":"Any","operator-roamingPreference-affiliated":"Affiliated","apn":"APN","apnSettings":"APN Settings","apnSettings-header":{"value":[{"type":"idOrVar","name":"apnSettings"}]},"dataSettings":"Data Settings","dataSettings-header":{"value":[{"type":"idOrVar","name":"dataSettings"}]},"suplSettings":"A-GPS Settings","suplSettings-header":{"value":[{"type":"idOrVar","name":"suplSettings"}]},"messageSettings":"Message Settings","messageSettings-header":{"value":[{"type":"idOrVar","name":"messageSettings"}]},"dunSettings":"Tethering Settings","dunSettings-header":{"value":[{"type":"idOrVar","name":"dunSettings"}]},"imsSettings":"IMS Settings","imsSettings-header2":{"value":[{"type":"idOrVar","name":"imsSettings"}]},"fotaSettings":"FOTA Settings","fotaSettings-header":{"value":[{"type":"idOrVar","name":"fotaSettings"}]},"apn-editor-header":"APN Editor","mmsport":"MMS port","mmsproxy":"MMS proxy","mmsc":"MMSC","httpProxyHost":"HTTP proxy host","httpProxyPort":"HTTP proxy port","custom":"(custom settings)","apn-authType":"Authentication","apn-authType-notDefined":"Not defined","apn-authType-none":"None","apn-authType-pap":"PAP","apn-authType-chap":"CHAP","apn-authType-papOrChap":"PAP or CHAP","apn-protocol":"Protocol","apn-roaming-protocol":"Roaming Protocol","apn-protocol-notDefined":"Not defined","apn-protocol-ip":"IPv4","apn-protocol-ipv6":"IPv6","apn-protocol-ipv4v6":"IPv4/IPv6","apn-type":"APN Type","apn-mtu":"MTU","dataConnection-warning-head":"Turn ON data connection?","dataConnection-warning-message":"Applications will automatically fetch data via data connection when required. Additional data charges may apply.","dataConnection-warning-message-short":"Automatically fetch mobile network data.","dataRoaming-warning-head":"Turn ON data roaming?","dataRoaming-warning-message":"Depending on your service agreement, extra charges may apply for data when you are in a roaming area.","dataRoaming-warning-message-short":"Fetch data when roaming.","change-apn-warning-message1":"Depending on your service agreement, changing your APN setting may result in extra charges when roaming. Do you want to proceed?","reset-apn-warning-title":"Reset to Default","reset-apn-warning-message":"This resets the settings to default values. This cannot be undone.","state-unknown":"Unknown state","state-available":"Available","state-connected":"Connected","state-current":"Connected","state-forbidden":"Forbidden","delete-apn-confirm":"Delete this APN?","delete-apn":"Delete","add-apn":"Add New APN","reset-apn":"Reset to Default","apnSettings-reset":"APN settings reset","bluetooth":"Bluetooth","bluetooth-header":{"value":[{"type":"idOrVar","name":"bluetooth"}]},"bluetooth-enable-msg":"Turn Bluetooth on to view devices in the area.","bluetooth-paired-devices":"Paired devices","bluetooth-devices-in-area":"Devices in the Area","bluetooth-visible-to-all":"Visible to All","bluetooth-name-maxlength-alert":{"value":["Your device name exceeds ",{"type":"idOrVar","name":"length"}," characters. Please retry again."]},"bluetooth-address":"Bluetooth Address","bluetooth-address-unavailable":"Not available","bt-status-nopaired":"No devices paired","bt-status-turnoff":"Turned off","bt-status-paired":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"zero":[{"type":"idOrVar","name":"name"}],"one":[{"type":"idOrVar","name":"name"},", +",{"type":"idOrVar","name":"n"}," more"],"two":[{"type":"idOrVar","name":"name"},", +",{"type":"idOrVar","name":"n"}," more"],"few":[{"type":"idOrVar","name":"name"},", +",{"type":"idOrVar","name":"n"}," more"],"many":[{"type":"idOrVar","name":"name"},", +",{"type":"idOrVar","name":"n"}," more"],"other":[{"type":"idOrVar","name":"name"},", +",{"type":"idOrVar","name":"n"}," more"]}},"device-status-tap-connect":"Tap to connect","device-status-pairing":"Pairing with device\u2026","device-status-waiting":"Waiting for other device\u2026","device-status-paired":"Paired","device-status-connecting":"Connecting\u2026","device-status-connected":"Connected","device-status-connected-media":"Connected to media audio","device-option-unpair":"Unpair","device-option-confirm":"Confirm","device-option-unpair-confirmation":"Confirmation","device-option-unpair-device":"Unpair device?","device-option-connect":"Connect","device-option-disconnect":"Disconnect","search-for-device":"Searching for devices\u2026","search-device":"Search for devices","rename-device":"Rename my device","unnamed-device":"Unknown device","change-btdevice-name":"Change device name","change-btdevice-name-desc":"This name will appear on your other Bluetooth devices","bluetooth-new-name":"New name","error-pair-title":"Unable to pair devices","error-pair-pincode":"Unable to pair with the device. Check that the PIN is correct.","error-pair-toofast":"Unable to pair with the device. Too many requests.","error-connect-title":"Unable to connect devices","error-connect-msg":"Check that the device you\u2019re trying to connect with is still in range and has Bluetooth turned on.","unpair-title":"Unpair a connected device?","unpair-msg":"You are currently connected to this device. Unpairing will also disconnect from it.","unpair-confirm":{"value":[{"type":"idOrVar","name":"unpair-title"},"\\n",{"type":"idOrVar","name":"unpair-msg"}]},"nfc":{"value":"NFC","attrs":{"ariaLabel":"NFC"}},"internetSharing":"Internet Sharing","internetSharing-header":{"value":[{"type":"idOrVar","name":"internetSharing"}]},"internetSharing-usb":"USB","internetSharing-wifi":"Wi-Fi","wifi-hotspot":"Wi-Fi Hotspot","wifi-hotspot-enabled-msg":{"value":["Hotspot \"",{"type":"idOrVar","name":"hotspotSSID"},"\" is enabled"]},"wifi-password":"Password","wifi-security":"Security","usb-tethering":"USB Tethering","hotspotSettings":"Hotspot Settings","wifi-auth-username":"Username","wifi-auth-password":"Password","wifi-join-username":"Username","wifi-join-password":"Password","hotspotSettings-header":{"value":[{"type":"idOrVar","name":"hotspotSettings"}]},"ssid-name":"SSID Network Name","hotspot-security-open":"open","hotspot-security-wpa-psk":"WPA","hotspot-security-wpa2-psk":"WPA2","hotspot-password-desc":"The password must have at least 8 characters.","personalization":"Personalization","sound":{"value":"Sound","attrs":{"ariaLabel":"Sound"}},"sound-header":{"value":[{"type":"idOrVar","name":"sound"}]},"volume":"Volume","tones":"Tones","touch-tones":"Audible touch tones","touch-tone-short":"Short","touch-tone-long":"Long","select-tone":"Select a tone","ringtones":"Ringtones","alerts":"Alerts","manage-tones":"Manage Tones","other-sounds":"Other Sounds","unlock-screen":"Unlock screen","camera":"Camera","sent-message":"Sent Message","alarm-sound":"Alarm","ringtone-and-notification-sound":"Ringtones & Notifications","media-sound":"Media","saving-tone":"Saving\u2026","no-ringtone-app":"We were unable to find a ringtone app to manage ringtones!","display":{"value":"Display","attrs":{"ariaLabel":"Display"}},"display-header":{"value":[{"type":"idOrVar","name":"display"}]},"brightness":"Brightness","brightness-autoAdjust":"Adjust Automatically","lockScreen":"Lock Screen","wallpaper":"Wallpaper","screen-timeout":"Screen Timeout","one-minute":"1 minute","two-minutes":"2 minutes","five-minutes":"5 minutes","ten-minutes":"10 minutes","screen-timeout-never":"never","lock-orientation":"Lock Orientation","navigation":{"value":"Navigation","attrs":{"ariaLabel":"Navigation"}},"navigation-header":{"value":[{"type":"idOrVar","name":"navigation"}]},"edge-swipe-navigation":"Edge Swipe Navigation","edge-swipe-navigation-enable-msg":"Drag from the left or right edge of the screen to jump between apps.","notifications":{"value":"Notifications","attrs":{"ariaLabel":"Notifications"}},"notifications-header":{"value":[{"type":"idOrVar","name":"notifications"}]},"lockscreen-notifications":"Show on Lock Screen","notifications-resend":"Show After Reboot","dateAndTime":"Date & Time","dateAndTime-header":{"value":[{"type":"idOrVar","name":"dateAndTime"}]},"setTimeAutomatically":"Set Automatically","timezoneMessage":"Time Zone","dateMessage":"Date","timeMessage":"Time","selectTimezoneContinent":"Select continent","selectTimezone":"Select time zone","time-format":"Format","time-format-time":"Time Format","useDefaultTimeFormat":"Use Default","hour-12":"12-hour","hour-24":"24-hour","languageAndRegion":"Language & Region","language":"Language","language-header":{"value":[{"type":"idOrVar","name":"language"}]},"region":"Region","more-languages":"Get More Languages","sample-format":"Sample Format","longDateFormat":"%A, %B %d, %Y","shortDateFormat":"%B %d, %Y","simManager":"SIM Manager","simManager-header":{"value":[{"type":"idOrVar","name":"simManager"}]},"simSettings":"SIM Settings","sim-manager-outgoing-calls":"Outgoing Calls","sim-manager-outgoing-call-desc":"Calls will be sent from","sim-manager-outgoing-messages":"Outgoing Messages","sim-manager-outgoing-messages-desc":"SMS will be sent from","sim-manager-outgoing-data":"Data","sim-manager-outgoing-data-description":"Data service network and Marketplace payments","sim-manager-always-ask":"Always ask","sim-pin-locked":"SIM PIN locked","no-operator":"No operator","cant-disable-simcard-alert":"At least one SIM should be activated at a time","disable-simcard-confirm":{"value":["SIM ",{"type":"idOrVar","name":"disableCardIndex"}," can\u2019t receive any calls and messages. Outgoing calls and outgoing messages which are set to SIM ",{"type":"idOrVar","name":"disableCardIndex"}," will be disabled."]},"change-outgoing-data-confirm":"The current data transfer will be suspended. Continue anyway?","home-screens":{"value":"Home Screens","attrs":{"ariaLabel":"Home Screens"}},"homescreen-layout":"Layout","homescreen-icon-layout":"Icon Layout","homescreen-three-columns":"Three Columns","homescreen-four-columns":"Four Columns","notice":"Notice","search":{"value":"Search","attrs":{"ariaLabel":"Search"}},"search-header":"Search","search-engine":"Search Engine","search-suggestions":"Search Suggestions","search-suggestions-description":"The characters you type when entering a search are sent to multiple search providers and will be used to send you search suggestions. Additional data charges may apply when using this feature.","replaceable-home-screen":"Replaceable Home Screen","change-home-screen":"Change Home Screen","get-more-home-screens":"Get More Home Screens","my-home-screens":"My Home Screens","home-screen-description":"Description","delete-home-screen-button":"Delete Home Screen","keyboardSettings":"Keyboard Settings","selectedKeyboards":"Selected Keyboards","keyboards":"Keyboards","keyboards-header":{"value":[{"type":"idOrVar","name":"keyboards"}]},"numberKeyboardLayouts":"Number layouts","optionKeyboardLayouts":"Option layouts","keypad":"Keypad","dialpad":"Dial Pad","selectKeyboards":"Select Keyboards","selectKeyboards-header":{"value":[{"type":"idOrVar","name":"selectKeyboards"}]},"mustHaveOneKeyboard":{"value":["At least one ",{"type":"idOrVar","name":"type"}," keyboard should be selected"]},"keyboardType-text":"text","keyboardType-url":"url","keyboardType-number":"number","defaultKeyboardEnabled":{"value":[{"type":"idOrVar","name":"appName"}," ",{"type":"idOrVar","name":"layoutName"}," is set as default input method"]},"keyboard-layout-emoji":"Emoji","themes":{"value":"Themes","attrs":{"ariaLabel":"Themes"}},"themes-header":{"value":[{"type":"idOrVar","name":"themes"}]},"fxa":"Firefox Accounts","accounts":"Account Management","fxa-accounts":"Firefox Accounts","fxa-accounts-header":{"value":[{"type":"idOrVar","name":"fxa-accounts"}]},"fxa-overview":"Your Firefox Account lets you use features like Find My Device and apps like the Firefox Marketplace.","fxa-invitation":"Create Account or Sign In","fxa-confirm-account":"Confirm Your Account","fxa-verification-email-sent-msg":{"value":["We will send an email to: ",{"type":"idOrVar","name":"email"}]},"fxa-please-verify":"Click the verification link in your email to confirm your account.","fxa-cancel-confirmation":"Cancel Account Confirmation","fxa-resend-alert":{"value":["A verification link has been sent to ",{"type":"idOrVar","name":"email"},"."]},"fxa-dont-see-email-resend":"Don\u2019t see an email? Resend.","fxa-about":"About","fxa-logged-in-text":{"value":["Signed in as ",{"type":"idOrVar","name":"email"}]},"fxa-options":"Options","fxa-sign-out":"Sign Out","fxa-confirm-email":{"value":["Please confirm ",{"type":"idOrVar","name":"email"}]},"findmydevice":"Find My Device","findmydevice-create-account":"Create account or sign in","findmydevice-enable":"Enable Find My Device","findmydevice-about":"About Find My Device","findmydevice-enable-geolocation-prompt":"Enable Geolocation?","findmydevice-enable-geolocation-description":"To view your device's location, you must enable geolocation tracking.","findmydevice-geolocation-enabled":"Geolocation Enabled","findmydevice-enable-network":"This device is currently offline. Connect to a network and try again.","findmydevice-error":"A Find My Device error has occurred.","findmydevice-please-try":"Please try again.","findmydevice-please-tap":"Please tap to try again.","fxsync-overview":"Sign in to sync your history, passwords & more","fxsync-get-started":"Get started","fxsync-sync-user":"Firefox Account","fxsync-disable":"Disable Firefox Sync","fxsync-last-synced":{"value":["Last synced ",{"type":"idOrVar","name":"when"}]},"fxsync-collections":"Collections","fxsync-legal":"Legal","privacyAndSecurity":"Privacy & Security","passcode-lock":"Passcode Lock","passcode-lock-desc":{"value":["Passcode: ",{"type":"idOrVar","name":"code"}]},"screenLock":"Screen Lock","screenLock-header":{"value":[{"type":"idOrVar","name":"screenLock"}]},"screenLock-enabled-with-no-passcode":"Enabled with no passcode","screenLock-enabled-with-passcode":"Enabled with passcode","simSecurity":"SIM Security","simSecurity-header":{"value":[{"type":"idOrVar","name":"simSecurity"}]},"appPermissions":{"value":"App Permissions","attrs":{"ariaLabel":"App Permissions"}},"appPermissions-header":{"value":[{"type":"idOrVar","name":"appPermissions"}]},"author":"Author","permissions":"Permissions","revoke":"Revoke","ask":"Ask","block":"Block","allow2":"Allow","uninstallApp":"Uninstall App","phone":"Phone","messages":"Messages","vibrate":"Vibrate","ring":"Ring","change":"Change","create":"Create","create-a-passcode":"Create a Passcode","passcode":"Passcode","passcode-heading":"Passcode","enter-passcode":"Enter passcode","new-passcode":"New passcode","current-passcode":"Current passcode","incorrect-passcode":"Passcode is incorrect","passcode-doesnt-match":"Passcode doesn\u2019t match. Try again.","confirm-passcode":"Confirm Passcode","immediately":"Immediately","after-one-minute":"After 1 minute","after-five-minutes":"After 5 minutes","after-fifteen-minutes":"After 15 minutes","after-thirty-minutes":"After 30 minutes","after-one-hour":"After 1 hour","require-passcode":"Require passcode","change-passcode":"Change passcode","noSimCard":"No SIM card","simPin":"SIM PIN","simPin2":"SIM PIN2","simPinWithIndex":{"value":["SIM ",{"type":"idOrVar","name":"index"}," PIN"]},"pukCode":"PUK code","puk2Code":"PUK2 code","whatIsSimPin":"What is a SIM PIN?","simPinIntro1":"A SIM PIN prevents access to the SIM card cellular data networks. When it\u2019s enabled, any device containing the SIM card will request the PIN upon restart.","simPinIntro2":"A SIM PIN is not the same as the passcode used to unlock the device.","changeSimPin":"Change PIN","simPinChangedSuccessfully":"SIM PIN changed successfully","simPinChangedSuccessfullyWithIndex":{"value":["SIM ",{"type":"idOrVar","name":"index"}," PIN changed successfully"]},"pinTitle":"Enter SIM PIN","pin2Title":"Enter SIM PIN2","pukTitle":"Enter PUK code","puk2Title":"Enter PUK2 code","newpinTitle":"New PIN","pinErrorMsg":"The PIN was incorrect.","inputCodeRetriesLeft":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"one":"Last try.","two":[{"type":"idOrVar","name":"n"}," tries left."],"few":[{"type":"idOrVar","name":"n"}," tries left."],"many":[{"type":"idOrVar","name":"n"}," tries left."],"other":[{"type":"idOrVar","name":"n"}," tries left."]}},"pinAttemptMsg3":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"one":"You have one try left to enter the correct code before locking the SIM card.","two":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before locking the SIM card."],"few":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before locking the SIM card."],"many":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before locking the SIM card."],"other":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before locking the SIM card."]}},"pinLastChanceMsg":"This is your last chance to enter the correct PIN. Otherwise, you must enter the PUK code to use this SIM card.","simCardLockedMsg":"The SIM card is locked.","simCardBlockedMsg":"The SIM card is blocked.","enterPukMsg":"You must enter the Personal Unlocking Key (PUK) code for the SIM card. Refer to your SIM card documentation or contact your carrier for more information.","enterPuk2Msg":"You must enter the Personal Unlocking Key (PUK2) code for the SIM card. Refer to your SIM card documentation or contact your carrier for more information.","pukErrorMsg":"The PUK code is incorrect.","pukAttemptMsg3":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"one":"You have one try left to enter the correct code before this SIM card will be permanently unusable. Refer to your SIM card documentation or contact your carrier for more information.","two":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before this SIM card will be permanently unusable. Refer to your SIM card documentation or contact your carrier for more information."],"few":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before this SIM card will be permanently unusable. Refer to your SIM card documentation or contact your carrier for more information."],"many":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before this SIM card will be permanently unusable. Refer to your SIM card documentation or contact your carrier for more information."],"other":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before this SIM card will be permanently unusable. Refer to your SIM card documentation or contact your carrier for more information."]}},"pukLastChanceMsg":"Last chance to enter the correct PUK code. Your SIM card will be permanently unusable if you enter in the wrong PUK code. Refer to your SIM card documentation or contact your carrier for more information.","newSimPinMsg":"Create PIN (must contain 4 to 8 digits)","newSimPin2Msg":"Create PIN2 (must contain 4 to 8 digits)","confirmNewSimPinMsg":"Confirm new PIN","confirmNewSimPin2Msg":"Confirm new PIN2","newPinErrorMsg":"PINs don\u2019t match","simLockedPhone":"SIM-locked phone","unknownSimCardState":"Unknown SIM card state","simCardNotReady":"SIM card not ready","simCardIllegal":"Unauthorized SIM card","genericLockError":"An unknown error occurred","doNotTrack":"Do Not Track","doNotTrack-header":{"value":[{"type":"idOrVar","name":"doNotTrack"}]},"doNotTrackActions":"Tell websites and apps that I do not want to be tracked","allowTracking":"Tell websites and apps that I want to be tracked","doNotHavePrefOnTracking":"Do not tell websites and apps anything about my tracking preferences","doNotTrack-dt":"How Does Do Not Track Work?","doNotTrack-dd1":"When you turn on Do Not Track, your device tells every website and app (as well as advertisers and other content providers) that you do not want your behavior tracked.","doNotTrack-dd2":"Turning on Do Not Track will not affect your ability to sign in to websites nor cause your device to forget your private information, such as the contents of shopping carts, location information or sign-in information.","browsingPrivacy":{"value":"Browsing Privacy","attrs":{"ariaLabel":"Browsing Privacy"}},"browsingPrivacy-header":{"value":[{"type":"idOrVar","name":"browsingPrivacy"}]},"confirm-clear-history-title":"Clear history","confirm-clear-history-desc":"Are you sure you want to clear your history? When using your Firefox Account, synced history will also be cleared.","confirm-clear-cookies-cache-title":"Delete cookies & cache","confirm-clear-cookies-cache-desc":"Are you sure you want to delete your cookies and cache?","clear":"Clear","privateBrowsing":"Private Browsing","privateBrowsingDetails":"Open all external links in new private windows.","trackingProtection":"Tracking Protection","trackingProtectionDetails":"Protect your browsing activity against potential trackers in all new windows.","browser-privacy-history-header":"History","browser-privacy-history-desc":"Clear your browsing history from this device. When using your Firefox Account, synced history will also be cleared.","browser-privacy-history-button":"Clear History","browser-privacy-cookies-cache-header":"Cookies & Cache","browser-privacy-cookies-cache-desc":"Delete stored data (such as cookies) from this device. Login information and preferences will no longer be remembered.","browser-privacy-cookies-cache-button":"Delete Cookies & Cache","storage":"Storage","enableUSBStorage1":"USB Storage","umsUnplugToDisable":"Unplug USB cable to disable","appStorage":"Application Storage","appStorage-header":{"value":[{"type":"idOrVar","name":"appStorage"}]},"storageSize":{"value":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}]},"availableSize":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"size"}]}],"value":{"zero":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," available"],"one":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," available"],"two":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," available"],"few":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," available"],"many":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," available"],"other":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," available"]}},"apps-total-space":"Total Space","apps-used-space":"Used","apps-free-space":"Left","storage-details":"Photos, videos and music are stored separately from apps. See media storage for details.","ums-shared-volumes":{"value":[{"type":"idOrVar","name":"list"}," shared"]},"transferProtocol-header":"Transfer Protocol","protocol-mtp":"Media Transfer Protocol (MTP)","protocol-ums":"USB Mass Storage (UMS)","mediaStorage":"Media Storage","mediaStorage-header":{"value":[{"type":"idOrVar","name":"mediaStorage"}]},"storage-name-internal":"Internal Storage","storage-name-external-0":"SD Card Storage","storage-name-external-1":"SD Card Storage 2","short-storage-name-internal":"Internal","short-storage-name-external-0":"SD Card","short-storage-name-external-1":"SD Card 2","music-space":"Music","pictures-space":"Pictures","videos-space":"Movies","left-space":"Space left","free-space":"Space left","total-space":"Total space","size-not-available":"Not available","no-storage":"No storage found","advanced":"Advanced","ums-warning-title":"Enable USB storage?","default-media-location":"Default Media Location","default-media-location-msg":"Choose where photos, videos, music and downloads are stored by default.","change-default-media-location-confirmation":"Changing the default media storage location will affect where all media will be saved by default. Existing data will remain in its current location. Do you want to proceed?","eject-sdcard-external-0":"Eject SD card","eject-sdcard-external-1":"Eject SD card 2","format-sdcard-internal":"Format internal storage","format-sdcard-external-0":"Format SD card","format-sdcard-external-1":"Format SD card 2","formatting":"Formatting\u2026","unmount-sdcard-confirmation":"You will not be able to access content on the SD card after you unmount it. Do you want to proceed?","format-sdcard-title":"Format SD card","format-sdcard-message":"This will erase all data stored on your SD card, such as music and photos.","format-sdcard-internal-title":"Format internal storage","format-sdcard-internal-message":"This will erase all data stored on your internal storage, such as music and photos.","format-sdcard-btnformat":"Format","sdcardUnmounted":"SD card unmounted","ums-partial-support-warning-title":"Warning: USB Mass Storage may not work","ums-partial-support-warning-msg-1":"USB Mass Storage will only work correctly with an external SD card. If you'd like to be able to access both your internal and external storage, you must use MTP.","ums-partial-support-warning-msg-2":"Press \"OK\" to switch to USB Mass Storage, or \"Cancel\" to remain in MTP mode.","device":"Device","help":{"value":"Help","attrs":{"ariaLabel":"Help"}},"deviceInfo":{"value":"Device Information","attrs":{"ariaLabel":"Device Information"}},"deviceInfo-header":{"value":[{"type":"idOrVar","name":"deviceInfo"}]},"deviceInfo-IMEI":"IMEI","deviceInfo-IMEI-with-index":{"value":["IMEI ",{"type":"idOrVar","name":"index"},": ",{"type":"idOrVar","name":"imei"},""]},"deviceInfo-ICCID":"ICCID","deviceInfo-ICCID-with-index":{"value":["SIM ",{"type":"idOrVar","name":"index"},": ",{"type":"idOrVar","name":"iccid"},""]},"deviceInfo-MSISDN":"Phone Number","deviceInfo-MSISDN-with-index":{"value":["SIM ",{"type":"idOrVar","name":"index"},": ",{"type":"idOrVar","name":"msisdn"},""]},"deviceInfo-ICCID-unavailable-sim":{"value":["SIM ",{"type":"idOrVar","name":"index"},": Not available"]},"downloads":{"value":"Downloads","attrs":{"ariaLabel":"Downloads"}},"downloads-header":{"value":[{"type":"idOrVar","name":"downloads"}]},"unknown-phoneNumber":"Unknown","unknown-phoneNumber-sim":{"value":["SIM ",{"type":"idOrVar","name":"index"},": Unknown"]},"gitInfo":"Git Commit Info","model-name":"Model","os-version":"OS Version","firmware_revision":"Firmware Revision","hardware_manufacturer":"Hardware Manufacturer","hardware_platform":"Hardware Platform","hardware_revision":"Hardware Revision","platform_version":"Platform Version","build-id":"Build Identifier","update-channel":"Update Channel","build_number":"Build Number","update-url":"Update URL","last-updated":"Last Updated","more-info":"More Information","more-info-header":{"value":[{"type":"idOrVar","name":"more-info"}]},"softwareUpdates":"Software Updates","check-update-daily":"Daily","check-update-weekly":"Weekly","check-update-monthly":"Monthly","check-for-updates":"Check for Updates","check-update-now":"Check Now","update-status":"Update status","no-network-when-update":"Connect to the Internet to proceed with your update.","checking-for-update":"Checking for updates\u2026","no-updates":"No updates were found","retry-when-online":"Network is offline. Will check again when the network is online.","already-latest-version":{"value":["This is already the latest version of ",{"type":"idOrVar","name":"brandShortName"}]},"check-error":"There was an error when checking for updates.","aboutBrowserOS":{"value":["About ",{"type":"idOrVar","name":"brandShortName"}]},"browser-os-desc":{"value":[{"type":"idOrVar","name":"brandShortName"}," is the free and open source operating system from Mozilla. Our mission is to promote openness, innovation and opportunity by keeping the power of the Web in your hands."]},"learn-more":"Learn More","reset-head":"Reset","reset-warning-title":"CAUTION","addons-auto-update":"Automatic add-on updates","addons-update-notify":"Notify me after install","your-rights":"Your Rights","your-rights-header":{"value":[{"type":"idOrVar","name":"your-rights"}]},"about-your-rights-0":{"value":[{"type":"idOrVar","name":"brandShortName"}," is built on free and open source software by a community of thousands from all over the world. There are a few things you should know:"]},"about-your-rights-1":{"value":[{"type":"idOrVar","name":"brandShortName"}," is made available to you under the terms of several open source licenses including the Mozilla Public License. A device running ",{"type":"idOrVar","name":"brandShortName"}," may also contain proprietary software from third parties. Any code provided under open licenses give you the right to modify the source code and distribute your modified versions as long as you comply with their terms."]},"about-your-rights-2":{"value":["You are not granted any trademark rights or licenses to the trademarks of the Mozilla Foundation or any party, including without limitation the ",{"type":"idOrVar","name":"brandShortName"}," name or logo."]},"about-your-rights-3":{"value":["Some features in ",{"type":"idOrVar","name":"brandShortName"},", such as the Crash Reporter, give you the option to provide feedback to Mozilla. By choosing to submit feedback, you give Mozilla permission to use the feedback to improve its products, to publish the feedback on its websites, and to distribute the feedback."]},"about-legal-info":"Legal Information","about-credit-info":"Credits","about-legal-info-header":{"value":[{"type":"idOrVar","name":"about-legal-info"}]},"open-source-notices":"Open Source Notices","open-source-notices-header":{"value":[{"type":"idOrVar","name":"open-source-notices"}]},"obtaining-source-code":"Obtaining Source Code","obtaining-source-code-header":{"value":[{"type":"idOrVar","name":"obtaining-source-code"}]},"about-mozilla":"Mozilla","about-other":"Other","your-privacy":"Your Privacy","your-privacy-header":{"value":[{"type":"idOrVar","name":"your-privacy"}]},"licensing-1":"Binaries of this product have been made available to you by the Mozilla Project, under the Mozilla Public License 2.0 (MPL). Know your rights.","licensing-2":"All of the source code to this product is available under licenses that are both free and open source. Most of it is available under the Mozilla Public License 2.0 (MPL).","licensing-3":"The remainder of the software that is not under the Mozilla Public License 2.0 MPL is available under one of a variety of other licenses. Those that require reproduction of the license text in the distribution are given below. (Note: Your copy of this product may not contain code covered by one or more of the licenses listed here, depending on the exact product and version you choose.)","licensing-MPL":"Mozilla Public License 2.0","licensing-Apache":"Apache License 2.0","developer":{"value":"Developer","attrs":{"ariaLabel":"Developer"}},"developer-header":{"value":[{"type":"idOrVar","name":"developer"}]},"hud-settings-button":"Developer HUD","hud-settings-header":{"value":[{"type":"idOrVar","name":"hud-settings-button"}]},"hud-devtools":"Developer Tools","hud-devtools-header":{"value":[{"type":"idOrVar","name":"hud-devtools"}]},"hud-hide":"Hide All HUD Elements","hud-logging":"Log Changes in ADB","hud-system":"Show System HUD","hud-problems":"Problems","hud-warnings":"Warnings","hud-errors":"Errors","hud-security":"Security Issues","hud-performance":"Performance","hud-reflows":"Reflows","hud-jank":"Jank","hud-jank-threshold":"Jank Threshold (ms)","hud-reflows-threshold":"Duration Threshold (ms)","hud-fps":"Frames per Second","hud-ttl":"Time to Load","hud-memory":"Memory","hud-uss":"Unique Set Size","hud-appmemory":"App Memory","hud-jsobjects":"JS Objects","hud-jsstrings":"JS Strings","hud-jsother":"JS Other","hud-dom":"DOM","hud-style":"Style","hud-other":"Other","software":"Software","devSettings":"Developer Settings","debug":"Debug","developer-menu":"Developer Menu","developer-tools":"Developer Tools","graphics":"Graphics","hardware-composer":"Hardware Composer","paint-flashing":"Flash Repainted Area","dumps-layers":"Dump Layers Tree","dumps-display-list":"Dump Display List","dumps-layerscope":"Dump LayerScope","apz-overscroll":"Overscrolling","tiling":"Tiling","low-precision":"Low Precision Painting","low-opacity":"Low Precision Transparency","draw-tile-borders":"Draw Tile Borders","draw-layer-borders":"Draw Layer Borders","log-animations":"Log Slow Animations","remote-debugging-usb":"Debugging via USB","devtools-wifi":"DevTools via Wi-Fi","devtools-device-name":"DevTools Wi-Fi Device Name","devtools-pseudolocalization":"Pseudo-Localization","debugger-disabled":"Disabled","debugger-adb-only":"ADB only","debugger-adb-and-devtools":"ADB and DevTools","wifi-debugging":"Wi-Fi Output in ADB","bluetooth-debugging":"Bluetooth Output in ADB","bluetooth-snoop-debugging":"Bluetooth HCI Sniffing Logs to SD Card","nfc-debugging":"NFC Output in ADB","ril-debugging":"RIL Output in ADB","network-debugging":"Network Output in ADB","mms-debugging":"MMS Output in ADB","console-enabled":"Console Enabled","cards-view-screenshots":"Cards View: Screenshots","geolocation-debugging-enabled":"Geolocation Output in ADB","geolocation-debugging-gps-locations-ignored":"Ignore GPS Locations","use-reviewer-certs":"Use Marketplace Reviewer Certs","shake-to-log":"Shake to Save System Log","enable-logshake-qa":"Enable LogShake QA Mode","launch-ftu":"Launch First Time Use","no-ftu":"No first time use app found","software-home-button":"Software Home Button","home-gesture":"Home Gesture","continuous-transition":"Continuous Transition","debug-gaia-enabled":"Gaia Debug Traces","app-transition":"App Transition","app-suspending":"App Suspending","copypaste":"Copy/Paste","multi-screen":"Multi-Screen","verbose-app-permissions":"Verbose App Permissions","pinning-the-web":"Enable Pinning the Web","view-source":"Enable View Source Gesture","homescreen-paging":"Enable Home Screen Paging","window-management":"Window Management","unlock-privileges":"Unlock Privileges","full-devtools":"Full DevTools","reset-devtools-warning-title":"CAUTION","service-worker":"Service Workers","service-worker-scope":"Scope","service-worker-script-spec":"Script spec","service-worker-current-worker-url":"Current worker URL","service-worker-active-cache-name":"Active cache name","service-worker-waiting-cache-name":"Waiting cache name","service-worker-update":"Update","service-worker-unregister":"Unregister","service-worker-no-registrations":"No service workers found","developer-menu-disabled-warning-title":"Developer menu not enabled","developer-menu-disabled-warning-msg-1":"Developer Mode can't be enabled without the developer menu first being enabled.","developer-menu-disabled-warning-msg-2":"Please enable it from Settings > Device Information > More Information > Developer Menu.","enable-full-dev-mode-first-warning-title":"Would you like to enable developer mode?","enable-full-dev-mode-first-warning-msg-1":"THIS WILL DELETE ALL YOUR DATA AND FACTORY RESET YOUR DEVICE.","enable-full-dev-mode-first-warning-msg-2":"THIS WILL DISABLE ALL SECURITY MEASURES ON YOUR DEVICE.","enable-full-dev-mode-final-warning-title":"Are you sure you want to enable developer mode?","enable-full-dev-mode-final-warning-msg":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"count"}]}],"value":{"one":["Tap 'Enable' ",{"type":"idOrVar","name":"count"}," more time to continue."],"two":["Tap 'Enable' ",{"type":"idOrVar","name":"count"}," more times to continue."],"few":["Tap 'Enable' ",{"type":"idOrVar","name":"count"}," more times to continue."],"many":["Tap 'Enable' ",{"type":"idOrVar","name":"count"}," more times to continue."],"other":["Tap 'Enable' ",{"type":"idOrVar","name":"count"}," more times to continue."]}},"downloads-edit":"Edit downloads","downloads-select-all":"Select all","downloads-deselect-all":"Deselect all","downloads-delete":"Delete","battery":"Battery","battery-header":{"value":[{"type":"idOrVar","name":"battery"}]},"battery-level-percent":{"value":[{"type":"idOrVar","name":"level"},"%"]},"battery-current-level":{"value":["Current Level (",{"type":"idOrVar","name":"level"},"%)"]},"battery-charging-calculating":"Calculating time required\u2026","battery-discharging-calculating":"Calculating time remaining\u2026","battery-charging-complete":"Fully charged","battery-discharging-complete":"Fully depleted","battery-time-hour":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"hours"}]}],"value":{"zero":[{"type":"idOrVar","name":"hours"}," hours"],"one":[{"type":"idOrVar","name":"hours"}," hour"],"two":[{"type":"idOrVar","name":"hours"}," hours"],"few":[{"type":"idOrVar","name":"hours"}," hours"],"many":[{"type":"idOrVar","name":"hours"}," hours"],"other":[{"type":"idOrVar","name":"hours"}," hours"]}},"battery-time-minute":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"minutes"}]}],"value":{"zero":[{"type":"idOrVar","name":"minutes"}," minutes"],"one":[{"type":"idOrVar","name":"minutes"}," minute"],"two":[{"type":"idOrVar","name":"minutes"}," minutes"],"few":[{"type":"idOrVar","name":"minutes"}," minutes"],"many":[{"type":"idOrVar","name":"minutes"}," minutes"],"other":[{"type":"idOrVar","name":"minutes"}," minutes"]}},"battery-charging-minutes":{"value":[{"type":"idOrVar","name":"battery-time-minute"}," until fully charged"]},"battery-charging-hours":{"value":[{"type":"idOrVar","name":"battery-time-hour"}," ",{"type":"idOrVar","name":"battery-time-minute"}," until fully charged"]},"battery-discharging-minutes":{"value":[{"type":"idOrVar","name":"battery-time-minute"}," remaining"]},"battery-discharging-hours":{"value":[{"type":"idOrVar","name":"battery-time-hour"}," ",{"type":"idOrVar","name":"battery-time-minute"}," remaining"]},"powerSaveModeTitle":"Power Save Mode","powerSaveMode":"Power Save Mode","powerSave-threshold":{"value":[{"type":"idOrVar","name":"level"},"% battery left"]},"powerSave-never":"Never","turnOnAuto":"Turn On Automatically","accessibility":{"value":"Accessibility","attrs":{"ariaLabel":"Accessibility"}},"accessibility-header":{"value":[{"type":"idOrVar","name":"accessibility"}]},"screenReader":"Screen Reader","screenReader-header":{"value":[{"type":"idOrVar","name":"screenReader"}]},"screenReader-toggle":{"value":[{"type":"idOrVar","name":"screenReader"}]},"colorFilter":"Color Filter","colorFilter-header":{"value":[{"type":"idOrVar","name":"colorFilter"}]},"colorFilter-toggle":{"value":[{"type":"idOrVar","name":"colorFilter"}]},"accessibility-audio":"Audio","accessibility-audio-monoAudio":"Mono Audio","mono-enabled":"Mono enabled","mono-disabled":"Mono disabled","screenReader-captions":"Display speech","screenReader-enable-msg":"When the screen reader is enabled, your device will provide spoken feedback to assist blind and low-vision users.","screen-reader-speech-volume":"Speech Volume","screen-reader-speech-rate":"Speech Rate","screenReader-confirm-title-start":"Start Screen Reader?","screenReader-confirm-description-start":"Activating the screen reader will change the way your touch screen works. Touch anywhere to hear the item under your finger, and double tap to activate.","screenReader-confirm-button-start":"Start","screenReader-confirm-title-stop":"Stop Screen Reader?","screenReader-confirm-description-stop":"Stopping the screen reader will end all speech feedback.","screenReader-confirm-button-stop":"Stop","colorFilter-invert":"Invert","colorFilter-grayscale":"Grayscale","colorFilter-contrast":"Contrast","accessibility-input":"Input","accessibility-input-desc":"Touch and gesture adjustments","tap-hold-delay":"Tap & Hold Delay","short-hold-delay":"Short","medium-hold-delay":"Medium","long-hold-delay":"Long","improveBrowserOS":{"value":["Improve ",{"type":"idOrVar","name":"brandShortName"}],"attrs":{"ariaLabel":{"value":["Improve ",{"type":"idOrVar","name":"brandShortName"}]}}},"improveBrowserOSTitle":{"value":["Improve ",{"type":"idOrVar","name":"brandShortName"}]},"sendMozillaFeedback":"Send Mozilla Feedback","performanceAndUsageData":"Performance and Usage Data","sharePerformanceAndUsageData":"Submit data","appUsageByRegion":"App Usage by Region","crashReports":"Crash Reports","crashReports-header":{"value":[{"type":"idOrVar","name":"crashReports"}]},"crashReportInfo":"Sending Mozilla a report when a crash occurs helps us fix the problem for everyone. Reports are sent over Wi-Fi only.","alwaysSendReport":"Always Send a Report","neverSendReport":"Never Send a Report","askToSendReport2":"Ask Each Time","crash-reports-description-1":"A crash report contains some details about the crash and your device, as well as a snapshot of the state of your device when it crashed.","crash-reports-description-2":"This may include things like open pages and apps, text typed into forms and the content of open messages, recent browsing history, or geolocation used by an open app.","crash-reports-description-3-start":"We use crash reports to try to fix problems and improve our products. We handle your information as we describe in our ","crash-reports-description-3-privacy":"privacy policy","crash-reports-description-3-end":".","sendFeedbackTitle":"Submit Your Feedback","feedback-description":"Your feedback will help us to improve Firefox OS.","feedback-happy":"Happy","feedback-sad":"Sad","feedback-mood-description":"How do you feel?","feedback_whyfeel_happy":"Why do you feel happy?","feedback_whyfeel_sad":"Why do you feel sad?","feedback-mood-explain":"Please tell us in detail.","feedback-public-description":"The content of your feedback will be public, so please be sure not include any personal information.","feedback-contactyou":"Check here to let us contact you from your email.","email-address1":"Email address:","email-keepprivate":"Don\u2019t worry. Your email will always be kept private.","feedback-sendbtn":"Send Feedback","feedback-errormessage-connect-error":"Sorry, the system can\u2019t send your suggestion due to a data connection error. Please try again when the issue is resolved.","feedback-errormessage-server-off":"Sorry, the system can\u2019t send your suggestion because the server shut down. Please wait a moment and try again.","feedback-errormessage-timeout":"Sorry, the system can\u2019t send your suggestion because of a timeout. Please restart and try again.","feedback-errormessage-wrong-email":"Sorry, the system can\u2019t send your suggestion because the email address is missing or due to a formatting error. Please use a valid email address and try again.","feedback-errormessage-empty-comment":"Sorry, the system can\u2019t send your suggestion because the comment field is empty. Please enter some text and try again.","feedback-complete-msg":"Thanks for the feedback! We use it to help create better experiences in future releases of Firefox OS.","feedback-errormessage-unknown-error":"Something went wrong. Please try again.","online-support":"Online support:","call-support":"Call support:","user-guide":"User Guide","default-support":"Please contact your local provider for support.","simToolkit-header":"SIM Toolkit","operatorServices":"Operator Services","operatorServices-help":"Help","operatorServices-helpmenu":"Help Menu","stkAppsNotAvailable":"SIM card applications not available.","stkItemsNaiSetUpCall":"Set Up Call","stkItemsNaiSendSs":"Send SS","stkItemsNaiSendUssd":"Send USSD","stkItemsNaiSendSms":"Send SMS","stkItemsNaiSendDtmf":"Send DTMF","stkItemsNaiLaunchBrowser":"Launch Browser","stkItemsNaiPlayTone":"Play Tone","stkItemsNaiDisplayText":"Display Text","stkItemsNaiGetInkey":"Get Inkey","stkItemsNaiGetInput":"Get Input","stkItemsNaiSelectItem":"Select Item","stkItemsNaiSetUpMenu":"Set Up Menu","stkItemsNaiProvideLocalInfo":"Provide Local Info","stkItemsNaiSetIdleModeText":"Set Idle Mode Text","stkItemsNaiPerformCardApdu":"Perform Card APDU","stkItemsNaiPowerOnCard":"Power On Card","stkItemsNaiPowerOffCard":"Power Off Card","stkItemsNaiGetReaderStatus":"Get Reader Status","stkItemsNaiOpenChannel":"Open Channel","stkItemsNaiCloseChannel":"Close Channel","stkItemsNaiReceiveData":"Receive Data","stkItemsNaiSendData":"Send Data","stkItemsNaiGetChannelStatus":"Get Channel Status","stkItemsNaiEndOfTheProactiveSession":"End of the proactive session","stkEntry":{"attrs":{"ariaLabel":{"value":[{"type":"idOrVar","name":"title"}]}}},"is-warning-storage-header":"USB storage","is-warning-tethering-header":"USB tethering","is-warning-wifi-header":"Wi-Fi hotspot","is-warning-storage-tethering-message":"Enabling USB storage will disable USB tethering. Do you want to enable USB storage?","is-warning-tethering-storage-message":"Enabling USB tethering will disable USB storage. Do you want to enable USB tethering?","is-warning-tethering-wifi-message":"Enabling USB tethering will disable Wi-Fi hotspot. Do you want to enable USB tethering?","is-warning-wifi-tethering-message":"Enabling Wi-Fi hotspot will disable USB tethering. Do you want to enable Wi-Fi hotspot?","settings-dialog-alert":"Alert","settings-dialog-confirm":"Confirm","settings-dialog-prompt":"Prompt","settings-alert-dialog-header":{"value":[{"type":"idOrVar","name":"settings-dialog-alert"}]},"settings-confirm-dialog-header":{"value":[{"type":"idOrVar","name":"settings-dialog-confirm"}]},"settings-prompt-dialog-header":{"value":[{"type":"idOrVar","name":"settings-dialog-prompt"}]},"accessibility-app-icon":{"attrs":{"ariaLabel":{"value":[{"type":"idOrVar","name":"appName"}," app"]}}},"addon":"Add-on","addons":{"value":"Add-ons","attrs":{"ariaLabel":"Add-ons"}},"addons-header":{"value":[{"type":"idOrVar","name":"addons"}]},"addon-details-header1":{"value":[{"type":"idOrVar","name":"appName"}]},"addon-enable":"Enable Add-on","addon-description-header":"Description","addon-developer-name":{"value":[{"type":"idOrVar","name":"developerName"}]},"addon-targets":"Affected Apps","addon-options":"Options","addon-delete":"Delete Add-on","addon-share":"Share Add-on","addon-share-no-provider":"No sharing app available","addon-no-description":"This add-on does not include a description in its manifest file.","addon-no-targets":"This add-on does not affect any installed apps.","addon-restart-apps-to-disable":"Add-on disabled; restart affected apps.","addon-reboot-required":"This add-on affected system apps; you must reboot your device to fully disable it.","addon-rename":"Rename","addon-rename-input":"Enter add-on name","addon-rename-desc":"Change the name of your add-on","addon-obsolete":"This add-on is obsolete and does not work.","addon-obsolete-can-delete":"This add-on is obsolete and does not work. Please delete it.","my-addons":"My Add-ons","get-more-addons":"Get More Add-ons","achievements":{"value":"Achievements","attrs":{"ariaLabel":"achievements"}},"achievements-customize-champ-name":"Customize Champ","achievements-customize-champ-description":"Open Customizer in an app","achievements-add-on-all-star-name":"Add-on All-Star","achievements-add-on-all-star-description":"Use Customizer to create your own add-on","achievements-fab-finder-name":"Fab Finder","achievements-fab-finder-description":"Install an app or add-on from Hackerplace","achievements-content-commander-name":"Content Commander","achievements-content-commander-description":"Submit an app or add-on to Hackerplace","achievements-sharing-is-caring-name":"Sharing is Caring","achievements-sharing-is-caring-description":"Share your apps and add-ons with the Sharing app","achievements-creative-kind-name":"Creative Kind","achievements-creative-kind-description":"Use Theme Editor to create a new theme","service-workers-interception":"Intercept requests","service-workers-testing":"Allow over HTTP","closeButton":{"attrs":{"ariaLabel":"Close"}},"changeWallpaperButton":{"attrs":{"ariaLabel":"Change wallpaper"}},"editButton":{"attrs":{"ariaLabel":"Edit"}},"wifiSection":{"attrs":{"ariaLabel":"Wi-Fi"}},"simManagerSection":{"attrs":{"ariaLabel":"SIM Manager"}},"callSettingsSection":{"attrs":{"ariaLabel":"Call Settings"}},"messagingSettingsSection":{"attrs":{"ariaLabel":"Messaging Settings"}},"cellularAndDataSection":{"attrs":{"ariaLabel":"Cellular & Data"}},"bluetoothSection":{"attrs":{"ariaLabel":"Bluetooth"}},"internetSharingSection":{"attrs":{"ariaLabel":"Internet Sharing"}},"dateAndTimeSection":{"attrs":{"ariaLabel":"Date & Time"}},"languageSection":{"attrs":{"ariaLabel":"Language"}},"keyboardsSection":{"attrs":{"ariaLabel":"Keyboards"}},"fxa-accountsSection":{"attrs":{"ariaLabel":"Firefox Accounts"}},"findmydeviceSection":{"attrs":{"ariaLabel":"Find My Device"}},"fxsyncSection":{"attrs":{"ariaLabel":"Firefox Sync"}},"screenLockSection":{"attrs":{"ariaLabel":"Screen Lock"}},"simSecuritySection":{"attrs":{"ariaLabel":"SIM Security"}},"doNotTrackSection":{"attrs":{"ariaLabel":"Do Not Track"}},"enableStorageSection":{"attrs":{"ariaLabel":"USB Storage"}},"mediaStorageSection":{"attrs":{"ariaLabel":"Media Storage"}},"appStorageSection":{"attrs":{"ariaLabel":"Application Storage"}},"batterySection":{"attrs":{"ariaLabel":"Battery"}},"helpImprove":"Help us improve our products and services by automatically sending analytics data. Data rates and charges may apply.","learn-more-privacy":"Read our Privacy Notice","basicMetricsRadio":"Basic","enhancedMetricsRadio":"Enhanced","noneMetricsRadio":"None","basicMetricsExplain":"Send app usage and error reports.","enhancedMetricsExplain":"Send Basic report plus diagnostic information.","noneMetricsExplain":"Do not send any data.","fxsync":"Firefox Sync","fxsync-collections-bookmarks":"Bookmarks","fxsync-collections-history":"History","fxsync-collections-passwords":"Passwords","fxsync-empty-account":"This account is empty. You need to sync from Firefox, Firefox for Android or Firefox for iOS first and try again here.","fxsync-error-invalid-account":"This account is inactive.","fxsync-error-invalid-account-explanation":"You need to activate Firefox Sync on Desktop or Android first and then try again.","fxsync-error-no-key-fetch-token":"Please try logging in again.","fxsync-error-no-key-fetch-token-explanation":"We found something wrong with your current Firefox Account session.","fxsync-error-offline":"No network connection.","fxsync-error-offline-explanation":"You need to be online to use Firefox Sync.","fxsync-error-unknown":"Oops. Something went wrong.","fxsync-error-unknown-explanation":"Sorry for the inconvenience. Please try again later.","fxsync-tos":"Terms of Service","fxsync-privacy":"Privacy Notice","fxsync-signing":"Signing in\u2026","fxsync-sync-now":"Sync now","fxsync-syncing":"Syncing","ringtone-none":"None","ringer_bubble":"Bubble","ringer_chimer":"Chimer","ringer_cunning":"Cunning","ringer_dulco":"Dulco","ringer_firefox2":{"value":[{"type":"idOrVar","name":"ringtoneBrandName"}]},"ringer_garden":"Garden","ringer_marbled":"Marbled","ringer_opti":"Opti","ringer_pecko":"Pecko","ringer_pingy":"Pingy","ringer_poppop":"Pop Pop","ringer_sata":"Sata","ringer_simple":"Clean","ringer_standard1":"Classic","ringer_standard2":"Classic Fast","ringer_stutter":"Stutter","ringer_symmetry":"Symmetry","ringer_tailwind":"Tail Wind","ringer_thoughtful":"Thoughtful","ringer_wit":"Wit","ringer_zen":"Zen","notifier_bells":"Bells","notifier_bloom":"Bloom","notifier_chime":"Chime","notifier_clicka":"Clicka","notifier_digi":"Digi","notifier_dropplet":"Dropplet","notifier_firefox2":{"value":[{"type":"idOrVar","name":"ringtoneBrandName"}]},"notifier_hello":"Hello","notifier_lowmarble":"Low Marble","notifier_ping":"Ping","notifier_plickle":"Plickle","notifier_pocko":"Pocko","notifier_quado":"Quado","notifier_red":"Red","notifier_sato":"Sato","notifier_swipo":"Swipo","notifier_trickleup":"Trickle Up","notifier_trink":"Trink","notifier_verse":"Verse","tz-region":"Region","tz-city":"City","tzRegion-Africa":"Africa","tzRegion-America":"America","tzRegion-Antarctica":"Antarctica","tzRegion-Asia":"Asia","tzRegion-Atlantic":"Atlantic Ocean","tzRegion-Australia":"Australia","tzRegion-Europe":"Europe","tzRegion-Indian":"Indian Ocean","tzRegion-Pacific":"Pacific Ocean","title-app":"App Permission","title-web":"Website Permission","title-cam":"Select Camera","perm-alarms":"Schedule Alarms","perm-audio-capture":"Microphone","perm-audio-capture-appRequest":{"value":["Would you like to share your microphone with ",{"type":"idOrVar","name":"app"},"?"]},"perm-audio-capture-webRequest":{"value":["Would you like to share your microphone with ",{"type":"idOrVar","name":"site"},"?"]},"perm-audio-channel-content":"Play Sounds","perm-audio-channel-notification":"Play Notification Sounds","perm-audio-channel-ringer":"Play Ringtone Sounds","perm-background-sensors":"Access Sensor Data","perm-bluetooth":"Bluetooth","perm-browser":"Inspect Web Pages","perm-camera":"Cameras","perm-camera-selector-appRequest":{"value":["Select a camera for ",{"type":"idOrVar","name":"app"},"."]},"perm-camera-selector-webRequest":{"value":["Select a camera for ",{"type":"idOrVar","name":"site"},"."]},"perm-camera-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to use your camera."]},"perm-camera-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to use your camera."]},"perm-cellbroadcast":"Cell Broadcasts","perm-contacts":"Contacts","perm-contacts-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to access your contact list."]},"perm-contacts-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to access your contact list."]},"perm-desktop-notification":"Notifications","perm-desktop-notification-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to display notifications."]},"perm-desktop-notification-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to display notifications."]},"perm-device-storage-apps":"Apps Storage","perm-device-storage-music":"Music Storage","perm-device-storage-music-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to access your music."]},"perm-device-storage-music-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to access your music."]},"perm-device-storage-pictures":"Image Storage","perm-device-storage-pictures-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to access your images."]},"perm-device-storage-pictures-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to access your images."]},"perm-device-storage-sdcard":"Memory Card Storage","perm-device-storage-sdcard-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to access your memory card."]},"perm-device-storage-sdcard-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to access your memory card."]},"perm-device-storage-videos":"Video Storage","perm-device-storage-videos-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to access your videos."]},"perm-device-storage-videos-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to access your videos."]},"perm-embed-apps":"Embed Apps","perm-fmradio":"FM Radio","perm-fmradio-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to use the FM Radio."]},"perm-fmradio-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to use the FM Radio."]},"perm-geolocation":"Geolocation","perm-geolocation-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to know your location."]},"perm-geolocation-more-info":{"value":[{"type":"idOrVar","name":"brandShortName"}," uses data such as GPS, Wi-Fi, and mobile networks near you to help determine your approximate location. Data is sent to Mozilla and other service providers and will be used in the aggregate to improve those location databases. Data may be stored on your device and data collection may occur even when no apps are running."]},"perm-geolocation-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to know your location."]},"perm-idle":"See Idle Status","perm-indexedDB-chrome-settings":"Privileged IndexedDB","perm-indexedDB-unlimited":"Unlimited IndexedDB Storage","perm-input-manage":"Manage Keyboards","perm-input":"Act as a Keyboard","perm-media-capture":"Camera and Microphone","perm-media-capture-appRequest":{"value":["Would you like to share your camera and microphone with ",{"type":"idOrVar","name":"app"},"?"]},"perm-media-capture-webRequest":{"value":["Would you like to share your camera and microphone with ",{"type":"idOrVar","name":"site"},"?"]},"perm-mobileconnection":"Mobile Connection","perm-mobileid":"Mobile Identity","perm-network-events":"Network Events","perm-open-remote-window":"Start an App","perm-permissions":"Modify Apps Permissions","perm-power":"Power Settings","perm-settings":"Modify Settings","perm-systemXHR":"Cross-Origin Requests","perm-telephony":"Telephony","perm-video-capture":"Camera","perm-video-capture-appRequest":{"value":["Would you like to share your camera with ",{"type":"idOrVar","name":"app"},"?"]},"perm-video-capture-webRequest":{"value":["Would you like to share your camera with ",{"type":"idOrVar","name":"site"},"?"]},"perm-voicemail":"Control Voicemail","perm-webapps-manage":"Install Webapps","perm-wifi-manage":"Manage Wi-Fi","perm-attention":"Open Overlay Popups","perm-audio-channel-alarm":"Play Alarm Sounds","perm-audio-channel-normal":"Play Sounds","perm-audio-channel-publicnotification":"Play public notification sounds","perm-audio-channel-telephony":"Play Telephony Sounds","perm-backgroundservice":"Act as a Background Service","perm-device-storage-crashes":"Crash Data Storage","perm-downloads":"Access Downloads","perm-embed-widgets":"Embed Widgets","perm-feature-detection":"Feature Detection","perm-firefox-accounts":"Firefox Accounts","perm-mobilenetwork":"Mobile Network","perm-moz-attention":"Open Overlay Popups (moz)","perm-moz-audio-channel-ringer":"Play Ringtone Sounds (moz)","perm-moz-audio-channel-telephony":"Play Telephony Sounds (moz)","perm-moz-firefox-accounts":"Firefox Accounts (moz)","perm-networkstats-manage":"Control Usage Stats","perm-nfc":"NFC","perm-nfc-hci-events":"NFC HCI Events","perm-nfc-manager":"Manage NFC","perm-phonenumberservice":"Phone Number Service","perm-push":"Push API","perm-resourcestats-manage":"Resource Stats","perm-sms":"Send and Read SMS","perm-speaker-control":"Control Volume","perm-storage":"Access Storage","perm-tcp-socket":"TCP Sockets","perm-test-permission":"Test Permission","perm-time":"Set Date and Time","perm-udp-socket":"UDP Sockets","perm-wappush":"WAP Push","weekday-0-long":"Sunday","weekday-1-long":"Monday","weekday-2-long":"Tuesday","weekday-3-long":"Wednesday","weekday-4-long":"Thursday","weekday-5-long":"Friday","weekday-6-long":"Saturday","weekday-0-short":"Sun","weekday-1-short":"Mon","weekday-2-short":"Tue","weekday-3-short":"Wed","weekday-4-short":"Thu","weekday-5-short":"Fri","weekday-6-short":"Sat","month-0-long":"January","month-1-long":"February","month-2-long":"March","month-3-long":"April","month-4-long":"May","month-5-long":"June","month-6-long":"July","month-7-long":"August","month-8-long":"September","month-9-long":"October","month-10-long":"November","month-11-long":"December","month-0-genitive":"January","month-1-genitive":"February","month-2-genitive":"March","month-3-genitive":"April","month-4-genitive":"May","month-5-genitive":"June","month-6-genitive":"July","month-7-genitive":"August","month-8-genitive":"September","month-9-genitive":"October","month-10-genitive":"November","month-11-genitive":"December","month-0-short":"Jan","month-1-short":"Feb","month-2-short":"Mar","month-3-short":"Apr","month-4-short":"May","month-5-short":"Jun","month-6-short":"Jul","month-7-short":"Aug","month-8-short":"Sep","month-9-short":"Oct","month-10-short":"Nov","month-11-short":"Dec","minutes-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"just now","one":"a minute ago","two":[{"type":"idOrVar","name":"value"}," minutes ago"],"few":[{"type":"idOrVar","name":"value"}," minutes ago"],"many":[{"type":"idOrVar","name":"value"}," minutes ago"],"other":[{"type":"idOrVar","name":"value"}," minutes ago"]}},"hours-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"just now","one":"an hour ago","two":[{"type":"idOrVar","name":"value"}," hours ago"],"few":[{"type":"idOrVar","name":"value"}," hours ago"],"many":[{"type":"idOrVar","name":"value"}," hours ago"],"other":[{"type":"idOrVar","name":"value"}," hours ago"]}},"days-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"today","one":"yesterday","two":[{"type":"idOrVar","name":"value"}," days ago"],"few":[{"type":"idOrVar","name":"value"}," days ago"],"many":[{"type":"idOrVar","name":"value"}," days ago"],"other":[{"type":"idOrVar","name":"value"}," days ago"]}},"weeks-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"this week","one":"last week","two":[{"type":"idOrVar","name":"value"}," weeks ago"],"few":[{"type":"idOrVar","name":"value"}," weeks ago"],"many":[{"type":"idOrVar","name":"value"}," weeks ago"],"other":[{"type":"idOrVar","name":"value"}," weeks ago"]}},"months-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"this month","one":"last month","two":[{"type":"idOrVar","name":"value"}," months ago"],"few":[{"type":"idOrVar","name":"value"}," months ago"],"many":[{"type":"idOrVar","name":"value"}," months ago"],"other":[{"type":"idOrVar","name":"value"}," months ago"]}},"years-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"this year","one":"last year","two":[{"type":"idOrVar","name":"value"}," years ago"],"few":[{"type":"idOrVar","name":"value"}," years ago"],"many":[{"type":"idOrVar","name":"value"}," years ago"],"other":[{"type":"idOrVar","name":"value"}," years ago"]}},"minutes-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"now","one":"in a minute","two":["in ",{"type":"idOrVar","name":"value"}," minutes"],"few":["in ",{"type":"idOrVar","name":"value"}," minutes"],"many":["in ",{"type":"idOrVar","name":"value"}," minutes"],"other":["in ",{"type":"idOrVar","name":"value"}," minutes"]}},"hours-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"now","one":"in an hour","two":["in ",{"type":"idOrVar","name":"value"}," hours"],"few":["in ",{"type":"idOrVar","name":"value"}," hours"],"many":["in ",{"type":"idOrVar","name":"value"}," hours"],"other":["in ",{"type":"idOrVar","name":"value"}," hours"]}},"days-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"today","one":"tomorrow","two":["in ",{"type":"idOrVar","name":"value"}," days"],"few":["in ",{"type":"idOrVar","name":"value"}," days"],"many":["in ",{"type":"idOrVar","name":"value"}," days"],"other":["in ",{"type":"idOrVar","name":"value"}," days"]}},"weeks-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"this week","one":"next week","two":["in ",{"type":"idOrVar","name":"value"}," weeks"],"few":["in ",{"type":"idOrVar","name":"value"}," weeks"],"many":["in ",{"type":"idOrVar","name":"value"}," weeks"],"other":["in ",{"type":"idOrVar","name":"value"}," weeks"]}},"months-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"this month","one":"next month","two":["in ",{"type":"idOrVar","name":"value"}," months"],"few":["in ",{"type":"idOrVar","name":"value"}," months"],"many":["in ",{"type":"idOrVar","name":"value"}," months"],"other":["in ",{"type":"idOrVar","name":"value"}," months"]}},"years-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"this year","one":"next year","two":["in ",{"type":"idOrVar","name":"value"}," years"],"few":["in ",{"type":"idOrVar","name":"value"}," years"],"many":["in ",{"type":"idOrVar","name":"value"}," years"],"other":["in ",{"type":"idOrVar","name":"value"}," years"]}},"minutes-ago-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"just now","other":[{"type":"idOrVar","name":"value"},"m ago"]}},"hours-ago-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"just now","other":[{"type":"idOrVar","name":"value"},"h ago"]}},"days-ago-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"today","other":[{"type":"idOrVar","name":"value"},"d ago"]}},"minutes-until-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"now","other":["in ",{"type":"idOrVar","name":"value"},"m"]}},"hours-until-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"now","other":["in ",{"type":"idOrVar","name":"value"},"h"]}},"days-until-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"today","other":["in ",{"type":"idOrVar","name":"value"},"d"]}},"incorrectDate":"(incorrect date)","dateTimeFormat_%c":"%a %b %e %Y %I:%M:%S %p","dateTimeFormat_%x":"%m/%d/%Y","dateTimeFormat_%X":"%I:%M:%S %p","shortTimeFormat":"%I:%M %p","shortTimeFormat12":"%I:%M %p","shortTimeFormat24":"%H:%M","shortDateTimeFormat":"%x %I:%M %p","weekStartsOnMonday":"0","firstDayOfTheWeek":"0","timePickerOrder":"HMp","datePickerOrder":"MDY","hourMinutesSeparator":":","listSeparator_middle":", ","time_am":"AM","time_pm":"PM","durationPattern":"hh:mm:ss.SS","brandShortName":"Vulpes","browserBrandShortName":"Vulpes","browserBrandFullName":"Project Vulpes \u2014 Firefox OS Community Project","ringtoneBrandName":"Default","ringtoneSectionBrandName":"System","stopped_download_title":"Download stopped","stopped_download_message":"Download was stopped. Try downloading again?","stopped_download_left_button":"Cancel","stopped_download_right_button":"Try again","failed_download_title":"Download failed","failed_download_message":{"value":[{"type":"idOrVar","name":"name"}," failed to download. Try downloading again?"]},"failed_download_left_button":"Cancel","failed_download_right_button":"Try again","stop_download_title":"Stop download","stop_download_message":"Are you sure you want to stop the download?","stop_download_left_button":"No","stop_download_right_button":"Yes","delete_download_title":"Delete","delete_download_message":{"value":["Delete ",{"type":"idOrVar","name":"name"},"?"]},"delete_download_left_button":"Cancel","delete_download_right_button":"Delete","delete_all_download_title":"Delete","delete_all_download_left_button":"Cancel","delete_all_download_right_button":"Delete","delete_all_download_message":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"number"}]}],"value":{"zero":"Delete no files?","one":"Delete one file?","two":["Delete ",{"type":"idOrVar","name":"number"}," files?"],"few":["Delete ",{"type":"idOrVar","name":"number"}," files?"],"many":["Delete ",{"type":"idOrVar","name":"number"}," files?"],"other":["Delete ",{"type":"idOrVar","name":"number"}," files?"]}},"unsupported_file_type_download_title":"Unable to open","unsupported_file_type_download_message":{"value":["There isn\u2019t an app that can open ",{"type":"idOrVar","name":"name"},". Would you like to save the file or delete it?"]},"unsupported_file_type_download_left_button":"Keep File","unsupported_file_type_download_right_button":"Delete","file_not_found_download_title":"File not found","file_not_found_download_message":"The file wasn\u2019t found on the device","file_not_found_download_right_button":"OK","unmounted_sdcard_2_download_title":"Memory card in use","unmounted_sdcard_2_download_message":"Unplug the phone","unmounted_sdcard_2_download_right_button":"OK","no_provider_download_title":"Unable to share","no_provider_download_message":"No provider found","no_provider_download_right_button":"OK","no_memory_download_title":"SD Card is full","no_memory_download_message":"There is no more room on your SD card to save your download. Please remove some files before restarting the download.","no_memory_download_right_button":"OK","no_sdcard_found_2_download_title":"No memory card found","no_sdcard_found_2_download_message":"Insert a memory card or select an alternate default media location in settings","no_sdcard_found_2_download_right_button":"OK","file_open_error_download_title":"Unable to open","file_open_error_download_message":{"value":["There is a problem opening ",{"type":"idOrVar","name":"name"},". Would you like to save the file or delete it?"]},"file_open_error_download_left_button":"Keep File","file_open_error_download_right_button":"Delete","restart_download_error":"There was an error while restarting the download. Please try to download again.","no-downloads":"No downloads","byteUnit-B":"B","byteUnit-KB":"KB","byteUnit-MB":"MB","byteUnit-GB":"GB","byteUnit-TB":"TB","byteUnit-PB":"PB","byteUnit-EB":"EB","byteUnit-ZB":"ZB","byteUnit-YB":"YB","fileSize":{"value":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}]},"percentage":{"value":[{"type":"idOrVar","name":"percentage"}," %"]},"partialResult":{"value":[{"type":"idOrVar","name":"partial"}," of ",{"type":"idOrVar","name":"total"}]},"summary":{"value":[{"type":"idOrVar","name":"date"}," - ",{"type":"idOrVar","name":"status"}]},"download-stopped":"Stopped","download-failed":"Download failed","open_downloaded_file":"Open","share_downloaded_file":"Share","wallpaper_downloaded_file":"Set as wallpaper","ringtone_downloaded_file":"Set as ringtone","cancel_downloaded_file":"Cancel"}
\ No newline at end of file
+{"gaia-menu-cancel":"Cancel","internetSharing-usb-description":"Share my Internet connection with a USB-connected device.","internetSharing-wifi-description":"Share my Internet connection with Wi-Fi-connected devices.","unplayable-ringtone":"The ringtone was not changed because the selected file can not be played on this phone.","findmydevice-signup-description":"Find My Device allows you to locate, lock or erase your phone","findmydevice-settings-description":"Find My Device allows you to locate, lock or erase your phone from a web site:","findmydevice-active-tracking":"Your phone is being tracked by another device through the website.","findmydevice-not-tracking":"No devices are actively tracking your phone through the website.","ums-confirm":"Enabling USB storage allows another computer or device connected using USB to access files on this phone.","reset-device":"Reset Phone","reset-warning-body":"Resetting will erase all your data, including any apps you’ve purchased. The phone will be restored to its factory condition. None of your settings or data will be saved.","reset-devtools-warning-body":"Resetting will erase all your data, including any apps you’ve purchased. None of your settings or data will be saved. But your phone will have unrestricted developer tools, allowing you to modify and debug system applications.","unreset-devtools-warning-body":"Resetting will erase all your data, including any apps you’ve purchased. None of your settings or data will be saved. Your phone will no longer have unrestricted developer tools. You will no longer be able to to modify or debug system applications.","powerSave-explanation":"Turning on power save mode turns off the phone’s data, Bluetooth and Geolocation connections to extend battery life. You can still turn these services back on manually.","performanceAndUsageDataInfo":{"value":["Help us improve ",{"type":"idOrVar","name":"brandShortName"}," by sharing data about your phone and the way you use it."]},"device-status-connected-device":"Connected to phone audio","device-status-connected-device-media":"Connected to phone/media audio","settings":"Settings","advancedSettings":"Advanced Settings","default":"Default","enabled":"Enabled","enable":"Enable","disabled":"Disabled","error":"Error","ok":"OK","yes":"Yes","no":"No","reset":"Reset","factory-reset":"Factory Reset","delete":"Delete","cancel":"Cancel","close":"Close","back":"Back","continue":"Continue","skip":"Skip","pair":"Pair","add":"Add","done":"Done","set":"Set","notNow":"Not now","turnOn":"Turn ON","selectASIMCard":"Select a SIM card","sim1":"SIM 1","sim2":"SIM 2","unavailable":"Not available","confirmation":"Confirmation","simSettingsWithIndex":{"value":["SIM ",{"type":"idOrVar","name":"index"}," Settings"]},"simWithIndex":{"value":["SIM ",{"type":"idOrVar","name":"index"}]},"networkAndConnectivity":"Network & Connectivity","airplaneMode":{"value":"Airplane Mode","attrs":{"ariaLabel":"Airplane Mode"}},"geolocation":{"value":"Geolocation","attrs":{"ariaLabel":"Geolocation"}},"wifi":"Wi-Fi","wifi-header":{"value":[{"type":"idOrVar","name":"wifi"}]},"wifi-disabled":"Turn Wi-Fi on to view available networks.","availableNetworks":"Available Networks","knownNetworks":"Known Networks","hiddenNetworks":"Hidden Networks","scanning":"Searching…","scanNetworks":"Search Again","full-status-initializing":"Initializing…","full-status-connecting":{"value":["Connecting to ",{"type":"idOrVar","name":"ssid"},"…"]},"full-status-associated":"Obtaining an IP address…","full-status-connected":{"value":["Connected to ",{"type":"idOrVar","name":"ssid"}]},"full-status-connectingfailed":"Connection failed","full-status-disconnected":"Not connected","short-status-connecting":"Connecting…","short-status-associated":"Obtaining an IP address…","short-status-connected":"Connected","short-status-connectingfailed":"Connection failed","short-status-disconnected":"Not connected","wifi-sleep":"Wi-Fi Sleep","wifi-sleep-description":"Enable to automatically turn off Wi-Fi during sleep to save battery power.","manageNetworks":"Manage Networks","wifi-bad-credentials-title":"Failed connecting to Wi-Fi","wifi-bad-credentials-confirm":{"value":["Unable to connect to ",{"type":"idOrVar","name":"ssid"},", authentication failed due to bad credentials. Please re-enter the network password."]},"manageNetworks-header":{"value":[{"type":"idOrVar","name":"manageNetworks"}]},"manageCertificates":"Manage Certificates","manageCertificates-header":{"value":[{"type":"idOrVar","name":"manageCertificates"}]},"joinHiddenNetwork":"Join Hidden Network","securedBy":{"value":["Secured by ",{"type":"idOrVar","name":"capabilities"}]},"security":"Security","securityNone":"None","securityOpen":"Open","notInRange":"Not in range","ipAddress":"IP address","linkSpeed":"Link speed","linkSpeedMbs":{"value":[{"type":"idOrVar","name":"linkSpeed"}," Mbit/s"]},"signalStrength":"Signal strength","signalLevel0":"Very weak","signalLevel1":"Weak","signalLevel2":"Average","signalLevel3":"Good","signalLevel4":"Very good","forget":"Forget","forgetNetwork-dialog":"Forget network?","forgetNetwork-confirmation":"Confirmation","authentication":"Authentication","authentication-header":{"value":[{"type":"idOrVar","name":"authentication"}]},"eap":"EAP method","auth-phase2":"Phase 2 authentication","server-certificate":"Server certificate","import-certificate-msg":"Notice: If you need to import new certificates, please go to \"Manage certificates\".","identity":"Identifier","password":"Password","showPassword":"Show password","networkNotification":"Network notification","networkNotification-expl":"Notify me when an open network is available","macAddress":"MAC Address","deviceInfo_software":{"value":[{"type":"idOrVar","name":"brandShortName"}," ",{"type":"idOrVar","name":"os"}]},"macUnavailable":"Not available","noNetworksFound":"No networks found","noKnownNetworks":"No known networks","edit":"Edit","certificateList":"Certificate List","noImportedCertificates":"No certificates imported","deleteCertificate":"Delete Certificate","importCertificate":"Import Certificate","selectCertificateFile":"Select Certificate File","enterCertificateNickname":"Enter certificate nick name","nickname":"Nick name","enter-certificate-name-msg":"You can change the nick name or keep the default name.","certificate-deletion-failed":"Certificate deletion failed","certificate-deletion-failed-description":"This may be due to a storage issue, for example.","certificate-import-failed":"Certificate import failed","certificate-import-failed-description":"This may be due to a damaged certificate file, for example.","wpsMessage":"Connect with WPS","wpsMessage-header":{"value":[{"type":"idOrVar","name":"wpsMessage"}]},"fullStatus-wps-inprogress":"WPS is in progress…","fullStatus-wps-canceled":"WPS was canceled","fullStatus-wps-timedout":"WPS timed out","fullStatus-wps-failed-error":{"value":["WPS failed [",{"type":"idOrVar","name":"error"},"]"]},"fullStatus-wps-overlapped":"Another WPS PBC AP was found","wpsDescription2":"Automatic Wi-Fi setup","wpsMethodSelection":"Select a WPS Method:","wpsPbcLabel":"Button connection","wpsMyPinLabel":"My PIN connection","wpsApPinLabel":"AP PIN connection","wpsPinDescription":"PIN is 4 or 8 digits","wpsPinInput":{"value":["Input ",{"type":"idOrVar","name":"pin"}," to an opposite device"]},"wpsPinAps":"Select an opposite device","wpsAnyAp":"Any","wpsCancelMessage":"Cancel WPS","wpsCancelFailedMessageError":{"value":["Failed to cancel WPS [",{"type":"idOrVar","name":"error"},"]"]},"callSettings":"Call Settings","callSettings-header":{"value":[{"type":"idOrVar","name":"callSettings"}]},"callSettingsQuery":"Requesting network info…","callSettingsQueryError":"Error in query","voiceMail":"Voicemail","voiceMail-header":{"value":[{"type":"idOrVar","name":"voiceMail"}]},"voiceMail-number":"Voicemail Number","voiceMail-number-notSet":"Not set","voicePrivacyMode":"Voice Privacy Mode","callWaiting":"Call Waiting","confirmCallWaitingTitle":"Unable to confirm call waiting preferences","confirmCallWaitingDesc":"The device is currently unable to communicate with the carrier. Try setting the preference again.","callForwarding":"Call Forwarding","callForwarding-header":{"value":[{"type":"idOrVar","name":"callForwarding"}]},"callForwardingForward":"Forward","callForwardingUnconditional":"Always Forward","callForwardingUnconditionalShort":"Always","callForwardingMobileBusy":"Forward When Busy","callForwardingMobileBusyShort":"When busy","callForwardingNoReply":"Forward When Unanswered","callForwardingNoReplyShort":"When unanswered","callForwardingNotReachable":"Forward When Unreachable","callForwardingNotReachableShort":"When unreachable","callForwardingForwardingVoiceToNumber":{"value":["Voice calls to ",{"type":"idOrVar","name":"number"},""]},"callForwardingNotForwarding":"Disabled","callForwardingConfirmTitle":"Call forwarding confirmation message","cf-alwaysUseThisNumber":"Always use this number","cf-numberWhenUnreachable":"Number when unreachable","cf-numberWhenUnanswered":"Number when unanswered","cf-numberWhenBusy":"Number when busy","callForwardingInvalidNumberError":"This phone number is invalid.","callForwardingSetSuccess":"Operation successfully set.","callForwardingSetForbidden":"Your operator does not support that operation.","callForwardingSetError":"Call settings error.","callerId":"Caller ID","callBarring":"Call Barring","callBarring-header":{"value":[{"type":"idOrVar","name":"callBarring"}]},"callBarring-outgoing":"Outgoing Calls","callBarring-baoc":"All","callBarring-boic":"International","callBarring-boicExhc":"International Except Home Country","callBarring-incoming":"Incoming Calls","callBarring-baic":"All","callBarring-baicR":"All While Roaming","callBarring-passcode":"Passcode","callBarring-change-passcode":"Change passcode","callBarring-passcode-enter":"Enter passcode","callBarring-passcode-unknown":"In case you don't know your passcode, please contact your carrier","callBarring-current-passcode":"Current passcode","callBarring-new-passcode":"New passcode","callBarring-create-passcode":"Create a Passcode","callBarring-confirm-passcode":"Confirm passcode","callBarring-passcode-error-generic":"Passcode error.","callBarring-passcode-error-incorrect":"Passcode is incorrect.","callBarring-passcode-error-mismatch":"Passcode doesn’t match.","callBarring-update-item-error":{"value":["Error while updating the item: ",{"type":"idOrVar","name":"error"},"."]},"callBarring-update-generic-error":"Network or passcode error","callBarring-change-passcode-success":"Passcode changed.","callerId-default":"Network default","callerId-hide":"Hide number","callerId-show":"Show number","fdn":"Fixed dialing numbers","fdnSettings":"Fixed Dialing Numbers","fdnSettings-header":{"value":[{"type":"idOrVar","name":"fdnSettings"}]},"fdn-authorizedNumbers":"Authorized Numbers","fdn-authorizedNumbers-header":{"value":[{"type":"idOrVar","name":"fdn-authorizedNumbers"}]},"fdn-authorizedNumbers-desc":"List of numbers that can be called","fdn-explanation1":"Restrict which numbers can be dialed from this phone.","fdn-explanation2":"(Emergency numbers will always be dialable)","fdnEnable":"Enable FDN","fdnDisable":"Disable FDN","fdnReset":"Reset PIN2","fdnAttemptMsg3":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"one":"You have one try left to enter the correct code before locking the SIM card.","two":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before locking the SIM card."],"few":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before locking the SIM card."],"many":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before locking the SIM card."],"other":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before locking the SIM card."]}},"fdnLastChanceMsg":"This is your last chance to enter the correct PIN2. Otherwise, you must enter the PUK2 code to use this SIM card.","fdnErrorMsg":"The PIN2 code is incorrect.","fdnContact-name":"name","fdnContact-number":"number","fdnContact-submit":"submit","fdnAction-add":"Add contact","fdnAction-call":"Call","fdnAction-edit":"Edit contact","fdnAction-edit-header":"Edit contact","fdnAction-delete":"Delete contact","fdnNoFDNFreeRecord":"Maximum FDN reached. You cannot add more numbers.","messagingSettings":"Messaging Settings","messagingSettings-header":{"value":[{"type":"idOrVar","name":"messagingSettings"}]},"message-confirmation":"Message Confirmation","message-delivery-reports":"Delivery Reports","message-delivery-reports-details":"Request notification of delivery for each message sent","message-read-reports":"Read Reports","message-read-reports-details":"Request read report for each message sent","mmsTitle":"Multimedia Messaging (MMS)","auto-retrieve":"Auto Retrieve","auto-retrieve-details":"Automatically retrieve messages","manual-retrieve":"Off","auto-roaming-retrieve":"On with roaming","auto-no-roaming-retrieve":"On without roaming","autoConfigure":"Auto-configure","wapPushMessages":"WAP Push Messages","wapPush":"WAP Push","cellBroadcastMessages":"Cell Broadcast Messages","cellBroadcast":"Cell Broadcast","emergencyAlert":"Emergency Alert","messagingInformation":"Information","messaging-SMSC":"Service Center","messaging-sim-settings":{"value":["SIM ",{"type":"idOrVar","name":"index"}," Settings"]},"unknown-SMSC":"Info not available","cellularAndData":"Cellular & Data","cellularAndData-header":{"value":[{"type":"idOrVar","name":"cellularAndData"}]},"dataOperator":"Operator","dataConnectivity":"Data","dataConnection":"Data Connection","dataRoaming":"Data Roaming","netOperator":"Operator","netOperator-header":{"value":[{"type":"idOrVar","name":"netOperator"}]},"availableOp":"Operators in the Area","preferredNetworkTypeAlertTitle":"Preferred network type","preferredNetworkTypeAlertErrorMessage":"Network type could not be set","operator-networkType":"Network Type","operator-networkType-auto":"Automatic","operator-networkType-prefer3G":"3G preferred","operator-networkType-2G":"2G only","operator-networkType-3G":"3G only","operator-networkType-GSM":"GSM","operator-networkType-WCDMA":"WCDMA","operator-networkType-CDMA":"CDMA","operator-networkType-EVDO":"EVDO","operator-networkType-LTE":"LTE","operator-networkType-preferWCDMA":"WCDMA preferred","operator-networkType-auto-2G-3G":"Auto (2G/3G)","operator-networkType-auto-3G-4G":"Auto (3G/4G)","operator-networkType-auto-CDMA-EVDO":"Auto (CDMA/EVDO)","operator-networkType-auto-WCDMA-GSM":"Auto (WCDMA/GSM)","operator-networkType-auto-LTE-WCDMA-GSM":"Auto (LTE/WCDMA/GSM)","operator-networkType-auto-LTE-WCDMA":"Auto (LTE/WCDMA)","operator-networkType-auto-LTE-CDMA-EVDO":"Auto (LTE/CDMA/EVDO)","operator-networkType-auto-WCDMA-GSM-CDMA-EVDO":"Auto (WCDMA/GSM/CDMA/EVDO)","operator-autoSelect":"Automatic Selection","operator-networkSelect-auto":"Automatic","operator-networkSelect-manual":"Manual","operator-turnAutoSelectionOff":"Turn automatic selection off to view available operators.","operator-info-connecting":"Connecting…","operator-info-failed":"Connection failed","operator-info-connected":"Connected","operator-info-available":"Available","operator-info-current":"Connected","operator-info-forbidden":"Forbidden","operator-info-unknown":"Unknown state","operator-roamingPreference":"Roaming preference","operator-roamingPreference-home":"Home","operator-roamingPreference-any":"Any","operator-roamingPreference-affiliated":"Affiliated","apn":"APN","apnSettings":"APN Settings","apnSettings-header":{"value":[{"type":"idOrVar","name":"apnSettings"}]},"dataSettings":"Data Settings","dataSettings-header":{"value":[{"type":"idOrVar","name":"dataSettings"}]},"suplSettings":"A-GPS Settings","suplSettings-header":{"value":[{"type":"idOrVar","name":"suplSettings"}]},"messageSettings":"Message Settings","messageSettings-header":{"value":[{"type":"idOrVar","name":"messageSettings"}]},"dunSettings":"Tethering Settings","dunSettings-header":{"value":[{"type":"idOrVar","name":"dunSettings"}]},"imsSettings":"IMS Settings","imsSettings-header2":{"value":[{"type":"idOrVar","name":"imsSettings"}]},"fotaSettings":"FOTA Settings","fotaSettings-header":{"value":[{"type":"idOrVar","name":"fotaSettings"}]},"apn-editor-header":"APN Editor","mmsport":"MMS port","mmsproxy":"MMS proxy","mmsc":"MMSC","httpProxyHost":"HTTP proxy host","httpProxyPort":"HTTP proxy port","custom":"(custom settings)","apn-authType":"Authentication","apn-authType-notDefined":"Not defined","apn-authType-none":"None","apn-authType-pap":"PAP","apn-authType-chap":"CHAP","apn-authType-papOrChap":"PAP or CHAP","apn-protocol":"Protocol","apn-roaming-protocol":"Roaming Protocol","apn-protocol-notDefined":"Not defined","apn-protocol-ip":"IPv4","apn-protocol-ipv6":"IPv6","apn-protocol-ipv4v6":"IPv4/IPv6","apn-type":"APN Type","apn-mtu":"MTU","dataConnection-warning-head":"Turn ON data connection?","dataConnection-warning-message":"Applications will automatically fetch data via data connection when required. Additional data charges may apply.","dataConnection-warning-message-short":"Automatically fetch mobile network data.","dataRoaming-warning-head":"Turn ON data roaming?","dataRoaming-warning-message":"Depending on your service agreement, extra charges may apply for data when you are in a roaming area.","dataRoaming-warning-message-short":"Fetch data when roaming.","change-apn-warning-message1":"Depending on your service agreement, changing your APN setting may result in extra charges when roaming. Do you want to proceed?","reset-apn-warning-title":"Reset to Default","reset-apn-warning-message":"This resets the settings to default values. This cannot be undone.","state-unknown":"Unknown state","state-available":"Available","state-connected":"Connected","state-current":"Connected","state-forbidden":"Forbidden","delete-apn-confirm":"Delete this APN?","delete-apn":"Delete","add-apn":"Add New APN","reset-apn":"Reset to Default","apnSettings-reset":"APN settings reset","bluetooth":"Bluetooth","bluetooth-header":{"value":[{"type":"idOrVar","name":"bluetooth"}]},"bluetooth-enable-msg":"Turn Bluetooth on to view devices in the area.","bluetooth-paired-devices":"Paired devices","bluetooth-devices-in-area":"Devices in the Area","bluetooth-visible-to-all":"Visible to All","bluetooth-name-maxlength-alert":{"value":["Your device name exceeds ",{"type":"idOrVar","name":"length"}," characters. Please retry again."]},"bluetooth-address":"Bluetooth Address","bluetooth-address-unavailable":"Not available","bt-status-nopaired":"No devices paired","bt-status-turnoff":"Turned off","bt-status-paired":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"zero":[{"type":"idOrVar","name":"name"}],"one":[{"type":"idOrVar","name":"name"},", +",{"type":"idOrVar","name":"n"}," more"],"two":[{"type":"idOrVar","name":"name"},", +",{"type":"idOrVar","name":"n"}," more"],"few":[{"type":"idOrVar","name":"name"},", +",{"type":"idOrVar","name":"n"}," more"],"many":[{"type":"idOrVar","name":"name"},", +",{"type":"idOrVar","name":"n"}," more"],"other":[{"type":"idOrVar","name":"name"},", +",{"type":"idOrVar","name":"n"}," more"]}},"device-status-tap-connect":"Tap to connect","device-status-pairing":"Pairing with device…","device-status-waiting":"Waiting for other device…","device-status-paired":"Paired","device-status-connecting":"Connecting…","device-status-connected":"Connected","device-status-connected-media":"Connected to media audio","device-option-unpair":"Unpair","device-option-confirm":"Confirm","device-option-unpair-confirmation":"Confirmation","device-option-unpair-device":"Unpair device?","device-option-connect":"Connect","device-option-disconnect":"Disconnect","search-for-device":"Searching for devices…","search-device":"Search for devices","rename-device":"Rename my device","unnamed-device":"Unknown device","change-btdevice-name":"Change device name","change-btdevice-name-desc":"This name will appear on your other Bluetooth devices","bluetooth-new-name":"New name","error-pair-title":"Unable to pair devices","error-pair-pincode":"Unable to pair with the device. Check that the PIN is correct.","error-pair-toofast":"Unable to pair with the device. Too many requests.","error-connect-title":"Unable to connect devices","error-connect-msg":"Check that the device you’re trying to connect with is still in range and has Bluetooth turned on.","unpair-title":"Unpair a connected device?","unpair-msg":"You are currently connected to this device. Unpairing will also disconnect from it.","unpair-confirm":{"value":[{"type":"idOrVar","name":"unpair-title"},"\\n",{"type":"idOrVar","name":"unpair-msg"}]},"nfc":{"value":"NFC","attrs":{"ariaLabel":"NFC"}},"internetSharing":"Internet Sharing","internetSharing-header":{"value":[{"type":"idOrVar","name":"internetSharing"}]},"internetSharing-usb":"USB","internetSharing-wifi":"Wi-Fi","wifi-hotspot":"Wi-Fi Hotspot","wifi-hotspot-enabled-msg":{"value":["Hotspot \"",{"type":"idOrVar","name":"hotspotSSID"},"\" is enabled"]},"wifi-password":"Password","wifi-security":"Security","usb-tethering":"USB Tethering","hotspotSettings":"Hotspot Settings","wifi-auth-username":"Username","wifi-auth-password":"Password","wifi-join-username":"Username","wifi-join-password":"Password","hotspotSettings-header":{"value":[{"type":"idOrVar","name":"hotspotSettings"}]},"ssid-name":"SSID Network Name","hotspot-security-open":"open","hotspot-security-wpa-psk":"WPA","hotspot-security-wpa2-psk":"WPA2","hotspot-password-desc":"The password must have at least 8 characters.","personalization":"Personalization","sound":{"value":"Sound","attrs":{"ariaLabel":"Sound"}},"sound-header":{"value":[{"type":"idOrVar","name":"sound"}]},"volume":"Volume","tones":"Tones","touch-tones":"Audible touch tones","touch-tone-short":"Short","touch-tone-long":"Long","select-tone":"Select a tone","ringtones":"Ringtones","alerts":"Alerts","manage-tones":"Manage Tones","other-sounds":"Other Sounds","unlock-screen":"Unlock screen","camera":"Camera","sent-message":"Sent Message","alarm-sound":"Alarm","ringtone-and-notification-sound":"Ringtones & Notifications","media-sound":"Media","saving-tone":"Saving…","no-ringtone-app":"We were unable to find a ringtone app to manage ringtones!","display":{"value":"Display","attrs":{"ariaLabel":"Display"}},"display-header":{"value":[{"type":"idOrVar","name":"display"}]},"brightness":"Brightness","brightness-autoAdjust":"Adjust Automatically","lockScreen":"Lock Screen","wallpaper":"Wallpaper","screen-timeout":"Screen Timeout","one-minute":"1 minute","two-minutes":"2 minutes","five-minutes":"5 minutes","ten-minutes":"10 minutes","screen-timeout-never":"never","lock-orientation":"Lock Orientation","navigation":{"value":"Navigation","attrs":{"ariaLabel":"Navigation"}},"navigation-header":{"value":[{"type":"idOrVar","name":"navigation"}]},"edge-swipe-navigation":"Edge Swipe Navigation","edge-swipe-navigation-enable-msg":"Drag from the left or right edge of the screen to jump between apps.","notifications":{"value":"Notifications","attrs":{"ariaLabel":"Notifications"}},"notifications-header":{"value":[{"type":"idOrVar","name":"notifications"}]},"lockscreen-notifications":"Show on Lock Screen","notifications-resend":"Show After Reboot","dateAndTime":"Date & Time","dateAndTime-header":{"value":[{"type":"idOrVar","name":"dateAndTime"}]},"setTimeAutomatically":"Set Automatically","timezoneMessage":"Time Zone","dateMessage":"Date","timeMessage":"Time","selectTimezoneContinent":"Select continent","selectTimezone":"Select time zone","time-format":"Format","time-format-time":"Time Format","useDefaultTimeFormat":"Use Default","hour-12":"12-hour","hour-24":"24-hour","languageAndRegion":"Language & Region","language":"Language","language-header":{"value":[{"type":"idOrVar","name":"language"}]},"region":"Region","more-languages":"Get More Languages","sample-format":"Sample Format","longDateFormat":"%A, %B %d, %Y","shortDateFormat":"%B %d, %Y","simManager":"SIM Manager","simManager-header":{"value":[{"type":"idOrVar","name":"simManager"}]},"simSettings":"SIM Settings","sim-manager-outgoing-calls":"Outgoing Calls","sim-manager-outgoing-call-desc":"Calls will be sent from","sim-manager-outgoing-messages":"Outgoing Messages","sim-manager-outgoing-messages-desc":"SMS will be sent from","sim-manager-outgoing-data":"Data","sim-manager-outgoing-data-description":"Data service network and Marketplace payments","sim-manager-always-ask":"Always ask","sim-pin-locked":"SIM PIN locked","no-operator":"No operator","cant-disable-simcard-alert":"At least one SIM should be activated at a time","disable-simcard-confirm":{"value":["SIM ",{"type":"idOrVar","name":"disableCardIndex"}," can’t receive any calls and messages. Outgoing calls and outgoing messages which are set to SIM ",{"type":"idOrVar","name":"disableCardIndex"}," will be disabled."]},"change-outgoing-data-confirm":"The current data transfer will be suspended. Continue anyway?","home-screens":{"value":"Home Screens","attrs":{"ariaLabel":"Home Screens"}},"homescreen-layout":"Layout","homescreen-icon-layout":"Icon Layout","homescreen-three-columns":"Three Columns","homescreen-four-columns":"Four Columns","notice":"Notice","search":{"value":"Search","attrs":{"ariaLabel":"Search"}},"search-header":"Search","search-engine":"Search Engine","search-suggestions":"Search Suggestions","search-suggestions-description":"The characters you type when entering a search are sent to multiple search providers and will be used to send you search suggestions. Additional data charges may apply when using this feature.","replaceable-home-screen":"Replaceable Home Screen","change-home-screen":"Change Home Screen","get-more-home-screens":"Get More Home Screens","my-home-screens":"My Home Screens","home-screen-description":"Description","delete-home-screen-button":"Delete Home Screen","keyboardSettings":"Keyboard Settings","selectedKeyboards":"Selected Keyboards","keyboards":"Keyboards","keyboards-header":{"value":[{"type":"idOrVar","name":"keyboards"}]},"numberKeyboardLayouts":"Number layouts","optionKeyboardLayouts":"Option layouts","keypad":"Keypad","dialpad":"Dial Pad","selectKeyboards":"Select Keyboards","selectKeyboards-header":{"value":[{"type":"idOrVar","name":"selectKeyboards"}]},"mustHaveOneKeyboard":{"value":["At least one ",{"type":"idOrVar","name":"type"}," keyboard should be selected"]},"keyboardType-text":"text","keyboardType-url":"url","keyboardType-number":"number","defaultKeyboardEnabled":{"value":[{"type":"idOrVar","name":"appName"}," ",{"type":"idOrVar","name":"layoutName"}," is set as default input method"]},"keyboard-layout-emoji":"Emoji","themes":{"value":"Themes","attrs":{"ariaLabel":"Themes"}},"themes-header":{"value":[{"type":"idOrVar","name":"themes"}]},"fxa":"Firefox Accounts","accounts":"Account Management","fxa-accounts":"Firefox Accounts","fxa-accounts-header":{"value":[{"type":"idOrVar","name":"fxa-accounts"}]},"fxa-overview":"Your Firefox Account lets you use features like Find My Device and apps like the Firefox Marketplace.","fxa-invitation":"Create Account or Sign In","fxa-confirm-account":"Confirm Your Account","fxa-verification-email-sent-msg":{"value":["We will send an email to: ",{"type":"idOrVar","name":"email"}]},"fxa-please-verify":"Click the verification link in your email to confirm your account.","fxa-cancel-confirmation":"Cancel Account Confirmation","fxa-resend-alert":{"value":["A verification link has been sent to ",{"type":"idOrVar","name":"email"},"."]},"fxa-dont-see-email-resend":"Don’t see an email? Resend.","fxa-about":"About","fxa-logged-in-text":{"value":["Signed in as ",{"type":"idOrVar","name":"email"}]},"fxa-options":"Options","fxa-sign-out":"Sign Out","fxa-confirm-email":{"value":["Please confirm ",{"type":"idOrVar","name":"email"}]},"findmydevice":"Find My Device","findmydevice-create-account":"Create account or sign in","findmydevice-enable":"Enable Find My Device","findmydevice-about":"About Find My Device","findmydevice-enable-geolocation-prompt":"Enable Geolocation?","findmydevice-enable-geolocation-description":"To view your device's location, you must enable geolocation tracking.","findmydevice-geolocation-enabled":"Geolocation Enabled","findmydevice-enable-network":"This device is currently offline. Connect to a network and try again.","findmydevice-error":"A Find My Device error has occurred.","findmydevice-please-try":"Please try again.","findmydevice-please-tap":"Please tap to try again.","fxsync-overview":"Sign in to sync your history, passwords & more","fxsync-get-started":"Get started","fxsync-sync-user":"Firefox Account","fxsync-disable":"Disable Firefox Sync","fxsync-last-synced":{"value":["Last synced ",{"type":"idOrVar","name":"when"}]},"fxsync-collections":"Collections","fxsync-legal":"Legal","privacyAndSecurity":"Privacy & Security","passcode-lock":"Passcode Lock","passcode-lock-desc":{"value":["Passcode: ",{"type":"idOrVar","name":"code"}]},"screenLock":"Screen Lock","screenLock-header":{"value":[{"type":"idOrVar","name":"screenLock"}]},"screenLock-enabled-with-no-passcode":"Enabled with no passcode","screenLock-enabled-with-passcode":"Enabled with passcode","simSecurity":"SIM Security","simSecurity-header":{"value":[{"type":"idOrVar","name":"simSecurity"}]},"appPermissions":{"value":"App Permissions","attrs":{"ariaLabel":"App Permissions"}},"appPermissions-header":{"value":[{"type":"idOrVar","name":"appPermissions"}]},"author":"Author","permissions":"Permissions","revoke":"Revoke","ask":"Ask","block":"Block","allow2":"Allow","uninstallApp":"Uninstall App","phone":"Phone","messages":"Messages","vibrate":"Vibrate","ring":"Ring","change":"Change","create":"Create","create-a-passcode":"Create a Passcode","passcode":"Passcode","passcode-heading":"Passcode","enter-passcode":"Enter passcode","new-passcode":"New passcode","current-passcode":"Current passcode","incorrect-passcode":"Passcode is incorrect","passcode-doesnt-match":"Passcode doesn’t match. Try again.","confirm-passcode":"Confirm Passcode","immediately":"Immediately","after-one-minute":"After 1 minute","after-five-minutes":"After 5 minutes","after-fifteen-minutes":"After 15 minutes","after-thirty-minutes":"After 30 minutes","after-one-hour":"After 1 hour","require-passcode":"Require passcode","change-passcode":"Change passcode","noSimCard":"No SIM card","simPin":"SIM PIN","simPin2":"SIM PIN2","simPinWithIndex":{"value":["SIM ",{"type":"idOrVar","name":"index"}," PIN"]},"pukCode":"PUK code","puk2Code":"PUK2 code","whatIsSimPin":"What is a SIM PIN?","simPinIntro1":"A SIM PIN prevents access to the SIM card cellular data networks. When it’s enabled, any device containing the SIM card will request the PIN upon restart.","simPinIntro2":"A SIM PIN is not the same as the passcode used to unlock the device.","changeSimPin":"Change PIN","simPinChangedSuccessfully":"SIM PIN changed successfully","simPinChangedSuccessfullyWithIndex":{"value":["SIM ",{"type":"idOrVar","name":"index"}," PIN changed successfully"]},"pinTitle":"Enter SIM PIN","pin2Title":"Enter SIM PIN2","pukTitle":"Enter PUK code","puk2Title":"Enter PUK2 code","newpinTitle":"New PIN","pinErrorMsg":"The PIN was incorrect.","inputCodeRetriesLeft":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"one":"Last try.","two":[{"type":"idOrVar","name":"n"}," tries left."],"few":[{"type":"idOrVar","name":"n"}," tries left."],"many":[{"type":"idOrVar","name":"n"}," tries left."],"other":[{"type":"idOrVar","name":"n"}," tries left."]}},"pinAttemptMsg3":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"one":"You have one try left to enter the correct code before locking the SIM card.","two":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before locking the SIM card."],"few":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before locking the SIM card."],"many":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before locking the SIM card."],"other":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before locking the SIM card."]}},"pinLastChanceMsg":"This is your last chance to enter the correct PIN. Otherwise, you must enter the PUK code to use this SIM card.","simCardLockedMsg":"The SIM card is locked.","simCardBlockedMsg":"The SIM card is blocked.","enterPukMsg":"You must enter the Personal Unlocking Key (PUK) code for the SIM card. Refer to your SIM card documentation or contact your carrier for more information.","enterPuk2Msg":"You must enter the Personal Unlocking Key (PUK2) code for the SIM card. Refer to your SIM card documentation or contact your carrier for more information.","pukErrorMsg":"The PUK code is incorrect.","pukAttemptMsg3":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"one":"You have one try left to enter the correct code before this SIM card will be permanently unusable. Refer to your SIM card documentation or contact your carrier for more information.","two":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before this SIM card will be permanently unusable. Refer to your SIM card documentation or contact your carrier for more information."],"few":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before this SIM card will be permanently unusable. Refer to your SIM card documentation or contact your carrier for more information."],"many":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before this SIM card will be permanently unusable. Refer to your SIM card documentation or contact your carrier for more information."],"other":["You have ",{"type":"idOrVar","name":"n"}," tries left to enter the correct code before this SIM card will be permanently unusable. Refer to your SIM card documentation or contact your carrier for more information."]}},"pukLastChanceMsg":"Last chance to enter the correct PUK code. Your SIM card will be permanently unusable if you enter in the wrong PUK code. Refer to your SIM card documentation or contact your carrier for more information.","newSimPinMsg":"Create PIN (must contain 4 to 8 digits)","newSimPin2Msg":"Create PIN2 (must contain 4 to 8 digits)","confirmNewSimPinMsg":"Confirm new PIN","confirmNewSimPin2Msg":"Confirm new PIN2","newPinErrorMsg":"PINs don’t match","simLockedPhone":"SIM-locked phone","unknownSimCardState":"Unknown SIM card state","simCardNotReady":"SIM card not ready","simCardIllegal":"Unauthorized SIM card","genericLockError":"An unknown error occurred","doNotTrack":"Do Not Track","doNotTrack-header":{"value":[{"type":"idOrVar","name":"doNotTrack"}]},"doNotTrackActions":"Tell websites and apps that I do not want to be tracked","allowTracking":"Tell websites and apps that I want to be tracked","doNotHavePrefOnTracking":"Do not tell websites and apps anything about my tracking preferences","doNotTrack-dt":"How Does Do Not Track Work?","doNotTrack-dd1":"When you turn on Do Not Track, your device tells every website and app (as well as advertisers and other content providers) that you do not want your behavior tracked.","doNotTrack-dd2":"Turning on Do Not Track will not affect your ability to sign in to websites nor cause your device to forget your private information, such as the contents of shopping carts, location information or sign-in information.","browsingPrivacy":{"value":"Browsing Privacy","attrs":{"ariaLabel":"Browsing Privacy"}},"browsingPrivacy-header":{"value":[{"type":"idOrVar","name":"browsingPrivacy"}]},"confirm-clear-history-title":"Clear history","confirm-clear-history-desc":"Are you sure you want to clear your history? When using your Firefox Account, synced history will also be cleared.","confirm-clear-cookies-cache-title":"Delete cookies & cache","confirm-clear-cookies-cache-desc":"Are you sure you want to delete your cookies and cache?","clear":"Clear","privateBrowsing":"Private Browsing","privateBrowsingDetails":"Open all external links in new private windows.","trackingProtection":"Tracking Protection","trackingProtectionDetails":"Protect your browsing activity against potential trackers in all new windows.","browser-privacy-history-header":"History","browser-privacy-history-desc":"Clear your browsing history from this device. When using your Firefox Account, synced history will also be cleared.","browser-privacy-history-button":"Clear History","browser-privacy-cookies-cache-header":"Cookies & Cache","browser-privacy-cookies-cache-desc":"Delete stored data (such as cookies) from this device. Login information and preferences will no longer be remembered.","browser-privacy-cookies-cache-button":"Delete Cookies & Cache","storage":"Storage","enableUSBStorage1":"USB Storage","umsUnplugToDisable":"Unplug USB cable to disable","appStorage":"Application Storage","appStorage-header":{"value":[{"type":"idOrVar","name":"appStorage"}]},"storageSize":{"value":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}]},"availableSize":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"size"}]}],"value":{"zero":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," available"],"one":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," available"],"two":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," available"],"few":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," available"],"many":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," available"],"other":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," available"]}},"apps-total-space":"Total Space","apps-used-space":"Used","apps-free-space":"Left","storage-details":"Photos, videos and music are stored separately from apps. See media storage for details.","ums-shared-volumes":{"value":[{"type":"idOrVar","name":"list"}," shared"]},"transferProtocol-header":"Transfer Protocol","protocol-mtp":"Media Transfer Protocol (MTP)","protocol-ums":"USB Mass Storage (UMS)","mediaStorage":"Media Storage","mediaStorage-header":{"value":[{"type":"idOrVar","name":"mediaStorage"}]},"storage-name-internal":"Internal Storage","storage-name-external-0":"SD Card Storage","storage-name-external-1":"SD Card Storage 2","short-storage-name-internal":"Internal","short-storage-name-external-0":"SD Card","short-storage-name-external-1":"SD Card 2","music-space":"Music","pictures-space":"Pictures","videos-space":"Movies","left-space":"Space left","free-space":"Space left","total-space":"Total space","size-not-available":"Not available","no-storage":"No storage found","advanced":"Advanced","ums-warning-title":"Enable USB storage?","default-media-location":"Default Media Location","default-media-location-msg":"Choose where photos, videos, music and downloads are stored by default.","change-default-media-location-confirmation":"Changing the default media storage location will affect where all media will be saved by default. Existing data will remain in its current location. Do you want to proceed?","eject-sdcard-external-0":"Eject SD card","eject-sdcard-external-1":"Eject SD card 2","format-sdcard-internal":"Format internal storage","format-sdcard-external-0":"Format SD card","format-sdcard-external-1":"Format SD card 2","formatting":"Formatting…","unmount-sdcard-confirmation":"You will not be able to access content on the SD card after you unmount it. Do you want to proceed?","format-sdcard-title":"Format SD card","format-sdcard-message":"This will erase all data stored on your SD card, such as music and photos.","format-sdcard-internal-title":"Format internal storage","format-sdcard-internal-message":"This will erase all data stored on your internal storage, such as music and photos.","format-sdcard-btnformat":"Format","sdcardUnmounted":"SD card unmounted","ums-partial-support-warning-title":"Warning: USB Mass Storage may not work","ums-partial-support-warning-msg-1":"USB Mass Storage will only work correctly with an external SD card. If you'd like to be able to access both your internal and external storage, you must use MTP.","ums-partial-support-warning-msg-2":"Press \"OK\" to switch to USB Mass Storage, or \"Cancel\" to remain in MTP mode.","device":"Device","help":{"value":"Help","attrs":{"ariaLabel":"Help"}},"deviceInfo":{"value":"Device Information","attrs":{"ariaLabel":"Device Information"}},"deviceInfo-header":{"value":[{"type":"idOrVar","name":"deviceInfo"}]},"deviceInfo-IMEI":"IMEI","deviceInfo-IMEI-with-index":{"value":["IMEI ",{"type":"idOrVar","name":"index"},": ",{"type":"idOrVar","name":"imei"},""]},"deviceInfo-ICCID":"ICCID","deviceInfo-ICCID-with-index":{"value":["SIM ",{"type":"idOrVar","name":"index"},": ",{"type":"idOrVar","name":"iccid"},""]},"deviceInfo-MSISDN":"Phone Number","deviceInfo-MSISDN-with-index":{"value":["SIM ",{"type":"idOrVar","name":"index"},": ",{"type":"idOrVar","name":"msisdn"},""]},"deviceInfo-ICCID-unavailable-sim":{"value":["SIM ",{"type":"idOrVar","name":"index"},": Not available"]},"downloads":{"value":"Downloads","attrs":{"ariaLabel":"Downloads"}},"downloads-header":{"value":[{"type":"idOrVar","name":"downloads"}]},"unknown-phoneNumber":"Unknown","unknown-phoneNumber-sim":{"value":["SIM ",{"type":"idOrVar","name":"index"},": Unknown"]},"gitInfo":"Git Commit Info","model-name":"Model","os-version":"OS Version","firmware_revision":"Firmware Revision","hardware_manufacturer":"Hardware Manufacturer","hardware_platform":"Hardware Platform","hardware_revision":"Hardware Revision","platform_version":"Platform Version","build-id":"Build Identifier","update-channel":"Update Channel","build_number":"Build Number","update-url":"Update URL","last-updated":"Last Updated","more-info":"More Information","more-info-header":{"value":[{"type":"idOrVar","name":"more-info"}]},"softwareUpdates":"Software Updates","check-update-daily":"Daily","check-update-weekly":"Weekly","check-update-monthly":"Monthly","check-for-updates":"Check for Updates","check-update-now":"Check Now","update-status":"Update status","no-network-when-update":"Connect to the Internet to proceed with your update.","checking-for-update":"Checking for updates…","no-updates":"No updates were found","retry-when-online":"Network is offline. Will check again when the network is online.","already-latest-version":{"value":["This is already the latest version of ",{"type":"idOrVar","name":"brandShortName"}]},"check-error":"There was an error when checking for updates.","aboutBrowserOS":{"value":["About ",{"type":"idOrVar","name":"brandShortName"}]},"browser-os-desc":{"value":[{"type":"idOrVar","name":"brandShortName"}," is the free and open source operating system from Mozilla. Our mission is to promote openness, innovation and opportunity by keeping the power of the Web in your hands."]},"learn-more":"Learn More","reset-head":"Reset","reset-warning-title":"CAUTION","addons-auto-update":"Automatic add-on updates","addons-update-notify":"Notify me after install","your-rights":"Your Rights","your-rights-header":{"value":[{"type":"idOrVar","name":"your-rights"}]},"about-your-rights-0":{"value":[{"type":"idOrVar","name":"brandShortName"}," is built on free and open source software by a community of thousands from all over the world. There are a few things you should know:"]},"about-your-rights-1":{"value":[{"type":"idOrVar","name":"brandShortName"}," is made available to you under the terms of several open source licenses including the Mozilla Public License. A device running ",{"type":"idOrVar","name":"brandShortName"}," may also contain proprietary software from third parties. Any code provided under open licenses give you the right to modify the source code and distribute your modified versions as long as you comply with their terms."]},"about-your-rights-2":{"value":["You are not granted any trademark rights or licenses to the trademarks of the Mozilla Foundation or any party, including without limitation the ",{"type":"idOrVar","name":"brandShortName"}," name or logo."]},"about-your-rights-3":{"value":["Some features in ",{"type":"idOrVar","name":"brandShortName"},", such as the Crash Reporter, give you the option to provide feedback to Mozilla. By choosing to submit feedback, you give Mozilla permission to use the feedback to improve its products, to publish the feedback on its websites, and to distribute the feedback."]},"about-legal-info":"Legal Information","about-credit-info":"Credits","about-legal-info-header":{"value":[{"type":"idOrVar","name":"about-legal-info"}]},"open-source-notices":"Open Source Notices","open-source-notices-header":{"value":[{"type":"idOrVar","name":"open-source-notices"}]},"obtaining-source-code":"Obtaining Source Code","obtaining-source-code-header":{"value":[{"type":"idOrVar","name":"obtaining-source-code"}]},"about-mozilla":"Mozilla","about-other":"Other","your-privacy":"Your Privacy","your-privacy-header":{"value":[{"type":"idOrVar","name":"your-privacy"}]},"licensing-1":"Binaries of this product have been made available to you by the Mozilla Project, under the Mozilla Public License 2.0 (MPL). Know your rights.","licensing-2":"All of the source code to this product is available under licenses that are both free and open source. Most of it is available under the Mozilla Public License 2.0 (MPL).","licensing-3":"The remainder of the software that is not under the Mozilla Public License 2.0 MPL is available under one of a variety of other licenses. Those that require reproduction of the license text in the distribution are given below. (Note: Your copy of this product may not contain code covered by one or more of the licenses listed here, depending on the exact product and version you choose.)","licensing-MPL":"Mozilla Public License 2.0","licensing-Apache":"Apache License 2.0","developer":{"value":"Developer","attrs":{"ariaLabel":"Developer"}},"developer-header":{"value":[{"type":"idOrVar","name":"developer"}]},"hud-settings-button":"Developer HUD","hud-settings-header":{"value":[{"type":"idOrVar","name":"hud-settings-button"}]},"hud-devtools":"Developer Tools","hud-devtools-header":{"value":[{"type":"idOrVar","name":"hud-devtools"}]},"hud-hide":"Hide All HUD Elements","hud-logging":"Log Changes in ADB","hud-system":"Show System HUD","hud-problems":"Problems","hud-warnings":"Warnings","hud-errors":"Errors","hud-security":"Security Issues","hud-performance":"Performance","hud-reflows":"Reflows","hud-jank":"Jank","hud-jank-threshold":"Jank Threshold (ms)","hud-reflows-threshold":"Duration Threshold (ms)","hud-fps":"Frames per Second","hud-ttl":"Time to Load","hud-memory":"Memory","hud-uss":"Unique Set Size","hud-appmemory":"App Memory","hud-jsobjects":"JS Objects","hud-jsstrings":"JS Strings","hud-jsother":"JS Other","hud-dom":"DOM","hud-style":"Style","hud-other":"Other","software":"Software","devSettings":"Developer Settings","debug":"Debug","developer-menu":"Developer Menu","developer-tools":"Developer Tools","graphics":"Graphics","hardware-composer":"Hardware Composer","paint-flashing":"Flash Repainted Area","dumps-layers":"Dump Layers Tree","dumps-display-list":"Dump Display List","dumps-layerscope":"Dump LayerScope","apz-overscroll":"Overscrolling","tiling":"Tiling","low-precision":"Low Precision Painting","low-opacity":"Low Precision Transparency","draw-tile-borders":"Draw Tile Borders","draw-layer-borders":"Draw Layer Borders","log-animations":"Log Slow Animations","remote-debugging-usb":"Debugging via USB","devtools-wifi":"DevTools via Wi-Fi","devtools-device-name":"DevTools Wi-Fi Device Name","devtools-pseudolocalization":"Pseudo-Localization","debugger-disabled":"Disabled","debugger-adb-only":"ADB only","debugger-adb-and-devtools":"ADB and DevTools","wifi-debugging":"Wi-Fi Output in ADB","bluetooth-debugging":"Bluetooth Output in ADB","bluetooth-snoop-debugging":"Bluetooth HCI Sniffing Logs to SD Card","nfc-debugging":"NFC Output in ADB","ril-debugging":"RIL Output in ADB","network-debugging":"Network Output in ADB","mms-debugging":"MMS Output in ADB","console-enabled":"Console Enabled","cards-view-screenshots":"Cards View: Screenshots","geolocation-debugging-enabled":"Geolocation Output in ADB","geolocation-debugging-gps-locations-ignored":"Ignore GPS Locations","use-reviewer-certs":"Use Marketplace Reviewer Certs","shake-to-log":"Shake to Save System Log","enable-logshake-qa":"Enable LogShake QA Mode","launch-ftu":"Launch First Time Use","no-ftu":"No first time use app found","software-home-button":"Software Home Button","home-gesture":"Home Gesture","continuous-transition":"Continuous Transition","debug-gaia-enabled":"Gaia Debug Traces","app-transition":"App Transition","app-suspending":"App Suspending","copypaste":"Copy/Paste","multi-screen":"Multi-Screen","verbose-app-permissions":"Verbose App Permissions","pinning-the-web":"Enable Pinning the Web","view-source":"Enable View Source Gesture","homescreen-paging":"Enable Home Screen Paging","window-management":"Window Management","unlock-privileges":"Unlock Privileges","full-devtools":"Full DevTools","reset-devtools-warning-title":"CAUTION","service-worker":"Service Workers","service-worker-scope":"Scope","service-worker-script-spec":"Script spec","service-worker-current-worker-url":"Current worker URL","service-worker-active-cache-name":"Active cache name","service-worker-waiting-cache-name":"Waiting cache name","service-worker-update":"Update","service-worker-unregister":"Unregister","service-worker-no-registrations":"No service workers found","developer-menu-disabled-warning-title":"Developer menu not enabled","developer-menu-disabled-warning-msg-1":"Developer Mode can't be enabled without the developer menu first being enabled.","developer-menu-disabled-warning-msg-2":"Please enable it from Settings > Device Information > More Information > Developer Menu.","enable-full-dev-mode-first-warning-title":"Would you like to enable developer mode?","enable-full-dev-mode-first-warning-msg-1":"THIS WILL DELETE ALL YOUR DATA AND FACTORY RESET YOUR DEVICE.","enable-full-dev-mode-first-warning-msg-2":"THIS WILL DISABLE ALL SECURITY MEASURES ON YOUR DEVICE.","enable-full-dev-mode-final-warning-title":"Are you sure you want to enable developer mode?","enable-full-dev-mode-final-warning-msg":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"count"}]}],"value":{"one":["Tap 'Enable' ",{"type":"idOrVar","name":"count"}," more time to continue."],"two":["Tap 'Enable' ",{"type":"idOrVar","name":"count"}," more times to continue."],"few":["Tap 'Enable' ",{"type":"idOrVar","name":"count"}," more times to continue."],"many":["Tap 'Enable' ",{"type":"idOrVar","name":"count"}," more times to continue."],"other":["Tap 'Enable' ",{"type":"idOrVar","name":"count"}," more times to continue."]}},"downloads-edit":"Edit downloads","downloads-select-all":"Select all","downloads-deselect-all":"Deselect all","downloads-delete":"Delete","battery":"Battery","battery-header":{"value":[{"type":"idOrVar","name":"battery"}]},"battery-level-percent":{"value":[{"type":"idOrVar","name":"level"},"%"]},"battery-current-level":{"value":["Current Level (",{"type":"idOrVar","name":"level"},"%)"]},"battery-charging-calculating":"Calculating time required…","battery-discharging-calculating":"Calculating time remaining…","battery-charging-complete":"Fully charged","battery-discharging-complete":"Fully depleted","battery-time-hour":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"hours"}]}],"value":{"zero":[{"type":"idOrVar","name":"hours"}," hours"],"one":[{"type":"idOrVar","name":"hours"}," hour"],"two":[{"type":"idOrVar","name":"hours"}," hours"],"few":[{"type":"idOrVar","name":"hours"}," hours"],"many":[{"type":"idOrVar","name":"hours"}," hours"],"other":[{"type":"idOrVar","name":"hours"}," hours"]}},"battery-time-minute":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"minutes"}]}],"value":{"zero":[{"type":"idOrVar","name":"minutes"}," minutes"],"one":[{"type":"idOrVar","name":"minutes"}," minute"],"two":[{"type":"idOrVar","name":"minutes"}," minutes"],"few":[{"type":"idOrVar","name":"minutes"}," minutes"],"many":[{"type":"idOrVar","name":"minutes"}," minutes"],"other":[{"type":"idOrVar","name":"minutes"}," minutes"]}},"battery-charging-minutes":{"value":[{"type":"idOrVar","name":"battery-time-minute"}," until fully charged"]},"battery-charging-hours":{"value":[{"type":"idOrVar","name":"battery-time-hour"}," ",{"type":"idOrVar","name":"battery-time-minute"}," until fully charged"]},"battery-discharging-minutes":{"value":[{"type":"idOrVar","name":"battery-time-minute"}," remaining"]},"battery-discharging-hours":{"value":[{"type":"idOrVar","name":"battery-time-hour"}," ",{"type":"idOrVar","name":"battery-time-minute"}," remaining"]},"powerSaveModeTitle":"Power Save Mode","powerSaveMode":"Power Save Mode","powerSave-threshold":{"value":[{"type":"idOrVar","name":"level"},"% battery left"]},"powerSave-never":"Never","turnOnAuto":"Turn On Automatically","accessibility":{"value":"Accessibility","attrs":{"ariaLabel":"Accessibility"}},"accessibility-header":{"value":[{"type":"idOrVar","name":"accessibility"}]},"screenReader":"Screen Reader","screenReader-header":{"value":[{"type":"idOrVar","name":"screenReader"}]},"screenReader-toggle":{"value":[{"type":"idOrVar","name":"screenReader"}]},"colorFilter":"Color Filter","colorFilter-header":{"value":[{"type":"idOrVar","name":"colorFilter"}]},"colorFilter-toggle":{"value":[{"type":"idOrVar","name":"colorFilter"}]},"accessibility-audio":"Audio","accessibility-audio-monoAudio":"Mono Audio","mono-enabled":"Mono enabled","mono-disabled":"Mono disabled","screenReader-captions":"Display speech","screenReader-enable-msg":"When the screen reader is enabled, your device will provide spoken feedback to assist blind and low-vision users.","screen-reader-speech-volume":"Speech Volume","screen-reader-speech-rate":"Speech Rate","screenReader-confirm-title-start":"Start Screen Reader?","screenReader-confirm-description-start":"Activating the screen reader will change the way your touch screen works. Touch anywhere to hear the item under your finger, and double tap to activate.","screenReader-confirm-button-start":"Start","screenReader-confirm-title-stop":"Stop Screen Reader?","screenReader-confirm-description-stop":"Stopping the screen reader will end all speech feedback.","screenReader-confirm-button-stop":"Stop","colorFilter-invert":"Invert","colorFilter-grayscale":"Grayscale","colorFilter-contrast":"Contrast","accessibility-input":"Input","accessibility-input-desc":"Touch and gesture adjustments","tap-hold-delay":"Tap & Hold Delay","short-hold-delay":"Short","medium-hold-delay":"Medium","long-hold-delay":"Long","improveBrowserOS":{"value":["Improve ",{"type":"idOrVar","name":"brandShortName"}],"attrs":{"ariaLabel":{"value":["Improve ",{"type":"idOrVar","name":"brandShortName"}]}}},"improveBrowserOSTitle":{"value":["Improve ",{"type":"idOrVar","name":"brandShortName"}]},"sendMozillaFeedback":"Send Mozilla Feedback","performanceAndUsageData":"Performance and Usage Data","sharePerformanceAndUsageData":"Submit data","appUsageByRegion":"App Usage by Region","crashReports":"Crash Reports","crashReports-header":{"value":[{"type":"idOrVar","name":"crashReports"}]},"crashReportInfo":"Sending Mozilla a report when a crash occurs helps us fix the problem for everyone. Reports are sent over Wi-Fi only.","alwaysSendReport":"Always Send a Report","neverSendReport":"Never Send a Report","askToSendReport2":"Ask Each Time","crash-reports-description-1":"A crash report contains some details about the crash and your device, as well as a snapshot of the state of your device when it crashed.","crash-reports-description-2":"This may include things like open pages and apps, text typed into forms and the content of open messages, recent browsing history, or geolocation used by an open app.","crash-reports-description-3-start":"We use crash reports to try to fix problems and improve our products. We handle your information as we describe in our ","crash-reports-description-3-privacy":"privacy policy","crash-reports-description-3-end":".","sendFeedbackTitle":"Submit Your Feedback","feedback-description":"Your feedback will help us to improve Firefox OS.","feedback-happy":"Happy","feedback-sad":"Sad","feedback-mood-description":"How do you feel?","feedback_whyfeel_happy":"Why do you feel happy?","feedback_whyfeel_sad":"Why do you feel sad?","feedback-mood-explain":"Please tell us in detail.","feedback-public-description":"The content of your feedback will be public, so please be sure not include any personal information.","feedback-contactyou":"Check here to let us contact you from your email.","email-address1":"Email address:","email-keepprivate":"Don’t worry. Your email will always be kept private.","feedback-sendbtn":"Send Feedback","feedback-errormessage-connect-error":"Sorry, the system can’t send your suggestion due to a data connection error. Please try again when the issue is resolved.","feedback-errormessage-server-off":"Sorry, the system can’t send your suggestion because the server shut down. Please wait a moment and try again.","feedback-errormessage-timeout":"Sorry, the system can’t send your suggestion because of a timeout. Please restart and try again.","feedback-errormessage-wrong-email":"Sorry, the system can’t send your suggestion because the email address is missing or due to a formatting error. Please use a valid email address and try again.","feedback-errormessage-empty-comment":"Sorry, the system can’t send your suggestion because the comment field is empty. Please enter some text and try again.","feedback-complete-msg":"Thanks for the feedback! We use it to help create better experiences in future releases of Firefox OS.","feedback-errormessage-unknown-error":"Something went wrong. Please try again.","online-support":"Online support:","call-support":"Call support:","user-guide":"User Guide","default-support":"Please contact your local provider for support.","simToolkit-header":"SIM Toolkit","operatorServices":"Operator Services","operatorServices-help":"Help","operatorServices-helpmenu":"Help Menu","stkAppsNotAvailable":"SIM card applications not available.","stkItemsNaiSetUpCall":"Set Up Call","stkItemsNaiSendSs":"Send SS","stkItemsNaiSendUssd":"Send USSD","stkItemsNaiSendSms":"Send SMS","stkItemsNaiSendDtmf":"Send DTMF","stkItemsNaiLaunchBrowser":"Launch Browser","stkItemsNaiPlayTone":"Play Tone","stkItemsNaiDisplayText":"Display Text","stkItemsNaiGetInkey":"Get Inkey","stkItemsNaiGetInput":"Get Input","stkItemsNaiSelectItem":"Select Item","stkItemsNaiSetUpMenu":"Set Up Menu","stkItemsNaiProvideLocalInfo":"Provide Local Info","stkItemsNaiSetIdleModeText":"Set Idle Mode Text","stkItemsNaiPerformCardApdu":"Perform Card APDU","stkItemsNaiPowerOnCard":"Power On Card","stkItemsNaiPowerOffCard":"Power Off Card","stkItemsNaiGetReaderStatus":"Get Reader Status","stkItemsNaiOpenChannel":"Open Channel","stkItemsNaiCloseChannel":"Close Channel","stkItemsNaiReceiveData":"Receive Data","stkItemsNaiSendData":"Send Data","stkItemsNaiGetChannelStatus":"Get Channel Status","stkItemsNaiEndOfTheProactiveSession":"End of the proactive session","stkEntry":{"attrs":{"ariaLabel":{"value":[{"type":"idOrVar","name":"title"}]}}},"is-warning-storage-header":"USB storage","is-warning-tethering-header":"USB tethering","is-warning-wifi-header":"Wi-Fi hotspot","is-warning-storage-tethering-message":"Enabling USB storage will disable USB tethering. Do you want to enable USB storage?","is-warning-tethering-storage-message":"Enabling USB tethering will disable USB storage. Do you want to enable USB tethering?","is-warning-tethering-wifi-message":"Enabling USB tethering will disable Wi-Fi hotspot. Do you want to enable USB tethering?","is-warning-wifi-tethering-message":"Enabling Wi-Fi hotspot will disable USB tethering. Do you want to enable Wi-Fi hotspot?","settings-dialog-alert":"Alert","settings-dialog-confirm":"Confirm","settings-dialog-prompt":"Prompt","settings-alert-dialog-header":{"value":[{"type":"idOrVar","name":"settings-dialog-alert"}]},"settings-confirm-dialog-header":{"value":[{"type":"idOrVar","name":"settings-dialog-confirm"}]},"settings-prompt-dialog-header":{"value":[{"type":"idOrVar","name":"settings-dialog-prompt"}]},"accessibility-app-icon":{"attrs":{"ariaLabel":{"value":[{"type":"idOrVar","name":"appName"}," app"]}}},"addon":"Add-on","addons":{"value":"Add-ons","attrs":{"ariaLabel":"Add-ons"}},"addons-header":{"value":[{"type":"idOrVar","name":"addons"}]},"addon-details-header1":{"value":[{"type":"idOrVar","name":"appName"}]},"addon-enable":"Enable Add-on","addon-description-header":"Description","addon-developer-name":{"value":[{"type":"idOrVar","name":"developerName"}]},"addon-targets":"Affected Apps","addon-options":"Options","addon-delete":"Delete Add-on","addon-share":"Share Add-on","addon-share-no-provider":"No sharing app available","addon-no-description":"This add-on does not include a description in its manifest file.","addon-no-targets":"This add-on does not affect any installed apps.","addon-restart-apps-to-disable":"Add-on disabled; restart affected apps.","addon-reboot-required":"This add-on affected system apps; you must reboot your device to fully disable it.","addon-rename":"Rename","addon-rename-input":"Enter add-on name","addon-rename-desc":"Change the name of your add-on","addon-obsolete":"This add-on is obsolete and does not work.","addon-obsolete-can-delete":"This add-on is obsolete and does not work. Please delete it.","my-addons":"My Add-ons","get-more-addons":"Get More Add-ons","achievements":{"value":"Achievements","attrs":{"ariaLabel":"achievements"}},"achievements-customize-champ-name":"Customize Champ","achievements-customize-champ-description":"Open Customizer in an app","achievements-add-on-all-star-name":"Add-on All-Star","achievements-add-on-all-star-description":"Use Customizer to create your own add-on","achievements-fab-finder-name":"Fab Finder","achievements-fab-finder-description":"Install an app or add-on from Hackerplace","achievements-content-commander-name":"Content Commander","achievements-content-commander-description":"Submit an app or add-on to Hackerplace","achievements-sharing-is-caring-name":"Sharing is Caring","achievements-sharing-is-caring-description":"Share your apps and add-ons with the Sharing app","achievements-creative-kind-name":"Creative Kind","achievements-creative-kind-description":"Use Theme Editor to create a new theme","service-workers-interception":"Intercept requests","service-workers-testing":"Allow over HTTP","closeButton":{"attrs":{"ariaLabel":"Close"}},"changeWallpaperButton":{"attrs":{"ariaLabel":"Change wallpaper"}},"editButton":{"attrs":{"ariaLabel":"Edit"}},"wifiSection":{"attrs":{"ariaLabel":"Wi-Fi"}},"simManagerSection":{"attrs":{"ariaLabel":"SIM Manager"}},"callSettingsSection":{"attrs":{"ariaLabel":"Call Settings"}},"messagingSettingsSection":{"attrs":{"ariaLabel":"Messaging Settings"}},"cellularAndDataSection":{"attrs":{"ariaLabel":"Cellular & Data"}},"bluetoothSection":{"attrs":{"ariaLabel":"Bluetooth"}},"internetSharingSection":{"attrs":{"ariaLabel":"Internet Sharing"}},"dateAndTimeSection":{"attrs":{"ariaLabel":"Date & Time"}},"languageSection":{"attrs":{"ariaLabel":"Language"}},"keyboardsSection":{"attrs":{"ariaLabel":"Keyboards"}},"fxa-accountsSection":{"attrs":{"ariaLabel":"Firefox Accounts"}},"findmydeviceSection":{"attrs":{"ariaLabel":"Find My Device"}},"fxsyncSection":{"attrs":{"ariaLabel":"Firefox Sync"}},"screenLockSection":{"attrs":{"ariaLabel":"Screen Lock"}},"simSecuritySection":{"attrs":{"ariaLabel":"SIM Security"}},"doNotTrackSection":{"attrs":{"ariaLabel":"Do Not Track"}},"enableStorageSection":{"attrs":{"ariaLabel":"USB Storage"}},"mediaStorageSection":{"attrs":{"ariaLabel":"Media Storage"}},"appStorageSection":{"attrs":{"ariaLabel":"Application Storage"}},"batterySection":{"attrs":{"ariaLabel":"Battery"}},"helpImprove":"Help us improve our products and services by automatically sending analytics data. Data rates and charges may apply.","learn-more-privacy":"Read our Privacy Notice","basicMetricsRadio":"Basic","enhancedMetricsRadio":"Enhanced","noneMetricsRadio":"None","basicMetricsExplain":"Send app usage and error reports.","enhancedMetricsExplain":"Send Basic report plus diagnostic information.","noneMetricsExplain":"Do not send any data.","fxsync":"Firefox Sync","fxsync-collections-bookmarks":"Bookmarks","fxsync-collections-history":"History","fxsync-collections-passwords":"Passwords","fxsync-empty-account":"This account is empty. You need to sync from Firefox, Firefox for Android or Firefox for iOS first and try again here.","fxsync-error-invalid-account":"This account is inactive.","fxsync-error-invalid-account-explanation":"You need to activate Firefox Sync on Desktop or Android first and then try again.","fxsync-error-no-key-fetch-token":"Please try logging in again.","fxsync-error-no-key-fetch-token-explanation":"We found something wrong with your current Firefox Account session.","fxsync-error-offline":"No network connection.","fxsync-error-offline-explanation":"You need to be online to use Firefox Sync.","fxsync-error-unknown":"Oops. Something went wrong.","fxsync-error-unknown-explanation":"Sorry for the inconvenience. Please try again later.","fxsync-tos":"Terms of Service","fxsync-privacy":"Privacy Notice","fxsync-signing":"Signing in…","fxsync-sync-now":"Sync now","fxsync-syncing":"Syncing","ringtone-none":"None","ringer_bubble":"Bubble","ringer_chimer":"Chimer","ringer_cunning":"Cunning","ringer_dulco":"Dulco","ringer_firefox2":{"value":[{"type":"idOrVar","name":"ringtoneBrandName"}]},"ringer_garden":"Garden","ringer_marbled":"Marbled","ringer_opti":"Opti","ringer_pecko":"Pecko","ringer_pingy":"Pingy","ringer_poppop":"Pop Pop","ringer_sata":"Sata","ringer_simple":"Clean","ringer_standard1":"Classic","ringer_standard2":"Classic Fast","ringer_stutter":"Stutter","ringer_symmetry":"Symmetry","ringer_tailwind":"Tail Wind","ringer_thoughtful":"Thoughtful","ringer_wit":"Wit","ringer_zen":"Zen","notifier_bells":"Bells","notifier_bloom":"Bloom","notifier_chime":"Chime","notifier_clicka":"Clicka","notifier_digi":"Digi","notifier_dropplet":"Dropplet","notifier_firefox2":{"value":[{"type":"idOrVar","name":"ringtoneBrandName"}]},"notifier_hello":"Hello","notifier_lowmarble":"Low Marble","notifier_ping":"Ping","notifier_plickle":"Plickle","notifier_pocko":"Pocko","notifier_quado":"Quado","notifier_red":"Red","notifier_sato":"Sato","notifier_swipo":"Swipo","notifier_trickleup":"Trickle Up","notifier_trink":"Trink","notifier_verse":"Verse","tz-region":"Region","tz-city":"City","tzRegion-Africa":"Africa","tzRegion-America":"America","tzRegion-Antarctica":"Antarctica","tzRegion-Asia":"Asia","tzRegion-Atlantic":"Atlantic Ocean","tzRegion-Australia":"Australia","tzRegion-Europe":"Europe","tzRegion-Indian":"Indian Ocean","tzRegion-Pacific":"Pacific Ocean","title-app":"App Permission","title-web":"Website Permission","title-cam":"Select Camera","perm-alarms":"Schedule Alarms","perm-audio-capture":"Microphone","perm-audio-capture-appRequest":{"value":["Would you like to share your microphone with ",{"type":"idOrVar","name":"app"},"?"]},"perm-audio-capture-webRequest":{"value":["Would you like to share your microphone with ",{"type":"idOrVar","name":"site"},"?"]},"perm-audio-channel-content":"Play Sounds","perm-audio-channel-notification":"Play Notification Sounds","perm-audio-channel-ringer":"Play Ringtone Sounds","perm-background-sensors":"Access Sensor Data","perm-bluetooth":"Bluetooth","perm-browser":"Inspect Web Pages","perm-camera":"Cameras","perm-camera-selector-appRequest":{"value":["Select a camera for ",{"type":"idOrVar","name":"app"},"."]},"perm-camera-selector-webRequest":{"value":["Select a camera for ",{"type":"idOrVar","name":"site"},"."]},"perm-camera-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to use your camera."]},"perm-camera-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to use your camera."]},"perm-cellbroadcast":"Cell Broadcasts","perm-contacts":"Contacts","perm-contacts-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to access your contact list."]},"perm-contacts-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to access your contact list."]},"perm-desktop-notification":"Notifications","perm-desktop-notification-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to display notifications."]},"perm-desktop-notification-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to display notifications."]},"perm-device-storage-apps":"Apps Storage","perm-device-storage-music":"Music Storage","perm-device-storage-music-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to access your music."]},"perm-device-storage-music-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to access your music."]},"perm-device-storage-pictures":"Image Storage","perm-device-storage-pictures-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to access your images."]},"perm-device-storage-pictures-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to access your images."]},"perm-device-storage-sdcard":"Memory Card Storage","perm-device-storage-sdcard-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to access your memory card."]},"perm-device-storage-sdcard-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to access your memory card."]},"perm-device-storage-videos":"Video Storage","perm-device-storage-videos-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to access your videos."]},"perm-device-storage-videos-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to access your videos."]},"perm-embed-apps":"Embed Apps","perm-fmradio":"FM Radio","perm-fmradio-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to use the FM Radio."]},"perm-fmradio-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to use the FM Radio."]},"perm-geolocation":"Geolocation","perm-geolocation-appRequest":{"value":[{"type":"idOrVar","name":"app"}," would like to know your location."]},"perm-geolocation-more-info":{"value":[{"type":"idOrVar","name":"brandShortName"}," uses data such as GPS, Wi-Fi, and mobile networks near you to help determine your approximate location. Data is sent to Mozilla and other service providers and will be used in the aggregate to improve those location databases. Data may be stored on your device and data collection may occur even when no apps are running."]},"perm-geolocation-webRequest":{"value":[{"type":"idOrVar","name":"site"}," would like to know your location."]},"perm-idle":"See Idle Status","perm-indexedDB-chrome-settings":"Privileged IndexedDB","perm-indexedDB-unlimited":"Unlimited IndexedDB Storage","perm-input-manage":"Manage Keyboards","perm-input":"Act as a Keyboard","perm-media-capture":"Camera and Microphone","perm-media-capture-appRequest":{"value":["Would you like to share your camera and microphone with ",{"type":"idOrVar","name":"app"},"?"]},"perm-media-capture-webRequest":{"value":["Would you like to share your camera and microphone with ",{"type":"idOrVar","name":"site"},"?"]},"perm-mobileconnection":"Mobile Connection","perm-mobileid":"Mobile Identity","perm-network-events":"Network Events","perm-open-remote-window":"Start an App","perm-permissions":"Modify Apps Permissions","perm-power":"Power Settings","perm-settings":"Modify Settings","perm-systemXHR":"Cross-Origin Requests","perm-telephony":"Telephony","perm-video-capture":"Camera","perm-video-capture-appRequest":{"value":["Would you like to share your camera with ",{"type":"idOrVar","name":"app"},"?"]},"perm-video-capture-webRequest":{"value":["Would you like to share your camera with ",{"type":"idOrVar","name":"site"},"?"]},"perm-voicemail":"Control Voicemail","perm-webapps-manage":"Install Webapps","perm-wifi-manage":"Manage Wi-Fi","perm-attention":"Open Overlay Popups","perm-audio-channel-alarm":"Play Alarm Sounds","perm-audio-channel-normal":"Play Sounds","perm-audio-channel-publicnotification":"Play public notification sounds","perm-audio-channel-telephony":"Play Telephony Sounds","perm-backgroundservice":"Act as a Background Service","perm-device-storage-crashes":"Crash Data Storage","perm-downloads":"Access Downloads","perm-embed-widgets":"Embed Widgets","perm-feature-detection":"Feature Detection","perm-firefox-accounts":"Firefox Accounts","perm-mobilenetwork":"Mobile Network","perm-moz-attention":"Open Overlay Popups (moz)","perm-moz-audio-channel-ringer":"Play Ringtone Sounds (moz)","perm-moz-audio-channel-telephony":"Play Telephony Sounds (moz)","perm-moz-firefox-accounts":"Firefox Accounts (moz)","perm-networkstats-manage":"Control Usage Stats","perm-nfc":"NFC","perm-nfc-hci-events":"NFC HCI Events","perm-nfc-manager":"Manage NFC","perm-phonenumberservice":"Phone Number Service","perm-push":"Push API","perm-resourcestats-manage":"Resource Stats","perm-sms":"Send and Read SMS","perm-speaker-control":"Control Volume","perm-storage":"Access Storage","perm-tcp-socket":"TCP Sockets","perm-test-permission":"Test Permission","perm-time":"Set Date and Time","perm-udp-socket":"UDP Sockets","perm-wappush":"WAP Push","weekday-0-long":"Sunday","weekday-1-long":"Monday","weekday-2-long":"Tuesday","weekday-3-long":"Wednesday","weekday-4-long":"Thursday","weekday-5-long":"Friday","weekday-6-long":"Saturday","weekday-0-short":"Sun","weekday-1-short":"Mon","weekday-2-short":"Tue","weekday-3-short":"Wed","weekday-4-short":"Thu","weekday-5-short":"Fri","weekday-6-short":"Sat","month-0-long":"January","month-1-long":"February","month-2-long":"March","month-3-long":"April","month-4-long":"May","month-5-long":"June","month-6-long":"July","month-7-long":"August","month-8-long":"September","month-9-long":"October","month-10-long":"November","month-11-long":"December","month-0-genitive":"January","month-1-genitive":"February","month-2-genitive":"March","month-3-genitive":"April","month-4-genitive":"May","month-5-genitive":"June","month-6-genitive":"July","month-7-genitive":"August","month-8-genitive":"September","month-9-genitive":"October","month-10-genitive":"November","month-11-genitive":"December","month-0-short":"Jan","month-1-short":"Feb","month-2-short":"Mar","month-3-short":"Apr","month-4-short":"May","month-5-short":"Jun","month-6-short":"Jul","month-7-short":"Aug","month-8-short":"Sep","month-9-short":"Oct","month-10-short":"Nov","month-11-short":"Dec","minutes-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"just now","one":"a minute ago","two":[{"type":"idOrVar","name":"value"}," minutes ago"],"few":[{"type":"idOrVar","name":"value"}," minutes ago"],"many":[{"type":"idOrVar","name":"value"}," minutes ago"],"other":[{"type":"idOrVar","name":"value"}," minutes ago"]}},"hours-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"just now","one":"an hour ago","two":[{"type":"idOrVar","name":"value"}," hours ago"],"few":[{"type":"idOrVar","name":"value"}," hours ago"],"many":[{"type":"idOrVar","name":"value"}," hours ago"],"other":[{"type":"idOrVar","name":"value"}," hours ago"]}},"days-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"today","one":"yesterday","two":[{"type":"idOrVar","name":"value"}," days ago"],"few":[{"type":"idOrVar","name":"value"}," days ago"],"many":[{"type":"idOrVar","name":"value"}," days ago"],"other":[{"type":"idOrVar","name":"value"}," days ago"]}},"weeks-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"this week","one":"last week","two":[{"type":"idOrVar","name":"value"}," weeks ago"],"few":[{"type":"idOrVar","name":"value"}," weeks ago"],"many":[{"type":"idOrVar","name":"value"}," weeks ago"],"other":[{"type":"idOrVar","name":"value"}," weeks ago"]}},"months-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"this month","one":"last month","two":[{"type":"idOrVar","name":"value"}," months ago"],"few":[{"type":"idOrVar","name":"value"}," months ago"],"many":[{"type":"idOrVar","name":"value"}," months ago"],"other":[{"type":"idOrVar","name":"value"}," months ago"]}},"years-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"this year","one":"last year","two":[{"type":"idOrVar","name":"value"}," years ago"],"few":[{"type":"idOrVar","name":"value"}," years ago"],"many":[{"type":"idOrVar","name":"value"}," years ago"],"other":[{"type":"idOrVar","name":"value"}," years ago"]}},"minutes-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"now","one":"in a minute","two":["in ",{"type":"idOrVar","name":"value"}," minutes"],"few":["in ",{"type":"idOrVar","name":"value"}," minutes"],"many":["in ",{"type":"idOrVar","name":"value"}," minutes"],"other":["in ",{"type":"idOrVar","name":"value"}," minutes"]}},"hours-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"now","one":"in an hour","two":["in ",{"type":"idOrVar","name":"value"}," hours"],"few":["in ",{"type":"idOrVar","name":"value"}," hours"],"many":["in ",{"type":"idOrVar","name":"value"}," hours"],"other":["in ",{"type":"idOrVar","name":"value"}," hours"]}},"days-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"today","one":"tomorrow","two":["in ",{"type":"idOrVar","name":"value"}," days"],"few":["in ",{"type":"idOrVar","name":"value"}," days"],"many":["in ",{"type":"idOrVar","name":"value"}," days"],"other":["in ",{"type":"idOrVar","name":"value"}," days"]}},"weeks-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"this week","one":"next week","two":["in ",{"type":"idOrVar","name":"value"}," weeks"],"few":["in ",{"type":"idOrVar","name":"value"}," weeks"],"many":["in ",{"type":"idOrVar","name":"value"}," weeks"],"other":["in ",{"type":"idOrVar","name":"value"}," weeks"]}},"months-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"this month","one":"next month","two":["in ",{"type":"idOrVar","name":"value"}," months"],"few":["in ",{"type":"idOrVar","name":"value"}," months"],"many":["in ",{"type":"idOrVar","name":"value"}," months"],"other":["in ",{"type":"idOrVar","name":"value"}," months"]}},"years-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"this year","one":"next year","two":["in ",{"type":"idOrVar","name":"value"}," years"],"few":["in ",{"type":"idOrVar","name":"value"}," years"],"many":["in ",{"type":"idOrVar","name":"value"}," years"],"other":["in ",{"type":"idOrVar","name":"value"}," years"]}},"minutes-ago-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"just now","other":[{"type":"idOrVar","name":"value"},"m ago"]}},"hours-ago-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"just now","other":[{"type":"idOrVar","name":"value"},"h ago"]}},"days-ago-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"today","other":[{"type":"idOrVar","name":"value"},"d ago"]}},"minutes-until-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"now","other":["in ",{"type":"idOrVar","name":"value"},"m"]}},"hours-until-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"now","other":["in ",{"type":"idOrVar","name":"value"},"h"]}},"days-until-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"today","other":["in ",{"type":"idOrVar","name":"value"},"d"]}},"incorrectDate":"(incorrect date)","dateTimeFormat_%c":"%a %b %e %Y %I:%M:%S %p","dateTimeFormat_%x":"%m/%d/%Y","dateTimeFormat_%X":"%I:%M:%S %p","shortTimeFormat":"%I:%M %p","shortTimeFormat12":"%I:%M %p","shortTimeFormat24":"%H:%M","shortDateTimeFormat":"%x %I:%M %p","weekStartsOnMonday":"0","firstDayOfTheWeek":"0","timePickerOrder":"HMp","datePickerOrder":"MDY","hourMinutesSeparator":":","listSeparator_middle":", ","time_am":"AM","time_pm":"PM","durationPattern":"hh:mm:ss.SS","brandShortName":"Vulpes","browserBrandShortName":"Vulpes","browserBrandFullName":"Project Vulpes — Firefox OS Community Project","ringtoneBrandName":"Default","ringtoneSectionBrandName":"System","stopped_download_title":"Download stopped","stopped_download_message":"Download was stopped. Try downloading again?","stopped_download_left_button":"Cancel","stopped_download_right_button":"Try again","failed_download_title":"Download failed","failed_download_message":{"value":[{"type":"idOrVar","name":"name"}," failed to download. Try downloading again?"]},"failed_download_left_button":"Cancel","failed_download_right_button":"Try again","stop_download_title":"Stop download","stop_download_message":"Are you sure you want to stop the download?","stop_download_left_button":"No","stop_download_right_button":"Yes","delete_download_title":"Delete","delete_download_message":{"value":["Delete ",{"type":"idOrVar","name":"name"},"?"]},"delete_download_left_button":"Cancel","delete_download_right_button":"Delete","delete_all_download_title":"Delete","delete_all_download_left_button":"Cancel","delete_all_download_right_button":"Delete","delete_all_download_message":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"number"}]}],"value":{"zero":"Delete no files?","one":"Delete one file?","two":["Delete ",{"type":"idOrVar","name":"number"}," files?"],"few":["Delete ",{"type":"idOrVar","name":"number"}," files?"],"many":["Delete ",{"type":"idOrVar","name":"number"}," files?"],"other":["Delete ",{"type":"idOrVar","name":"number"}," files?"]}},"unsupported_file_type_download_title":"Unable to open","unsupported_file_type_download_message":{"value":["There isn’t an app that can open ",{"type":"idOrVar","name":"name"},". Would you like to save the file or delete it?"]},"unsupported_file_type_download_left_button":"Keep File","unsupported_file_type_download_right_button":"Delete","file_not_found_download_title":"File not found","file_not_found_download_message":"The file wasn’t found on the device","file_not_found_download_right_button":"OK","unmounted_sdcard_2_download_title":"Memory card in use","unmounted_sdcard_2_download_message":"Unplug the phone","unmounted_sdcard_2_download_right_button":"OK","no_provider_download_title":"Unable to share","no_provider_download_message":"No provider found","no_provider_download_right_button":"OK","no_memory_download_title":"SD Card is full","no_memory_download_message":"There is no more room on your SD card to save your download. Please remove some files before restarting the download.","no_memory_download_right_button":"OK","no_sdcard_found_2_download_title":"No memory card found","no_sdcard_found_2_download_message":"Insert a memory card or select an alternate default media location in settings","no_sdcard_found_2_download_right_button":"OK","file_open_error_download_title":"Unable to open","file_open_error_download_message":{"value":["There is a problem opening ",{"type":"idOrVar","name":"name"},". Would you like to save the file or delete it?"]},"file_open_error_download_left_button":"Keep File","file_open_error_download_right_button":"Delete","restart_download_error":"There was an error while restarting the download. Please try to download again.","no-downloads":"No downloads","byteUnit-B":"B","byteUnit-KB":"KB","byteUnit-MB":"MB","byteUnit-GB":"GB","byteUnit-TB":"TB","byteUnit-PB":"PB","byteUnit-EB":"EB","byteUnit-ZB":"ZB","byteUnit-YB":"YB","fileSize":{"value":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}]},"percentage":{"value":[{"type":"idOrVar","name":"percentage"}," %"]},"partialResult":{"value":[{"type":"idOrVar","name":"partial"}," of ",{"type":"idOrVar","name":"total"}]},"summary":{"value":[{"type":"idOrVar","name":"date"}," - ",{"type":"idOrVar","name":"status"}]},"download-stopped":"Stopped","download-failed":"Download failed","open_downloaded_file":"Open","share_downloaded_file":"Share","wallpaper_downloaded_file":"Set as wallpaper","ringtone_downloaded_file":"Set as ringtone","cancel_downloaded_file":"Cancel","battery-charging-unavailable":"Charge time unavailable","battery-discharging-unavailable":"Time remaining unavailable"}
diff --git a/assets/webapps/settings.gaiamobile.org/locales-obj/index.fr.json b/assets/webapps/settings.gaiamobile.org/locales-obj/index.fr.json
index b92b7b6f..73162d91 100644
--- a/assets/webapps/settings.gaiamobile.org/locales-obj/index.fr.json
+++ b/assets/webapps/settings.gaiamobile.org/locales-obj/index.fr.json
@@ -1 +1 @@
-{"gaia-menu-cancel":"Annuler","internetSharing-usb-description":"Partager ma connexion Internet avec un appareil connect\u00e9 par USB.","internetSharing-wifi-description":"Partager ma connexion Internet avec d\u2019autres appareils connect\u00e9s par Wi-Fi.","unplayable-ringtone":"Impossible de changer la sonnerie car le fichier s\u00e9lectionn\u00e9 n\u2019est pas lisible par ce t\u00e9l\u00e9phone.","findmydevice-signup-description":"Localiser mon appareil vous permet de localiser, verrouiller ou effacer votre t\u00e9l\u00e9phone","findmydevice-settings-description":"Localiser mon appareil vous permet de localiser, verrouiller ou effacer votre t\u00e9l\u00e9phone depuis un site web\u00a0:","findmydevice-active-tracking":"Votre t\u00e9l\u00e9phone est localis\u00e9 par un autre appareil depuis le site web.","findmydevice-not-tracking":"Aucun appareil ne localise actuellement votre t\u00e9l\u00e9phone depuis le site web.","ums-confirm":"Activer le stockage USB permet \u00e0 un autre appareil ou ordinateur connect\u00e9 par USB d\u2019acc\u00e9der aux fichiers de ce t\u00e9l\u00e9phone.","reset-device":"R\u00e9initialiser le t\u00e9l\u00e9phone","reset-warning-body":"La r\u00e9initialisation effacera toutes vos donn\u00e9es, y compris les applications que vous avez achet\u00e9es. Le t\u00e9l\u00e9phone sera restaur\u00e9 aux valeurs d\u2019usine. Aucun de vos param\u00e8tres ni aucune de vos donn\u00e9es ne seront conserv\u00e9s.","reset-devtools-warning-body":"La r\u00e9initialisation effacera toutes vos donn\u00e9es, y compris les applications que vous avez achet\u00e9es. Aucun de vos param\u00e8tres ni aucune de vos donn\u00e9es ne seront sauvegard\u00e9s. Cependant votre t\u00e9l\u00e9phone poss\u00e8dera des outils de d\u00e9veloppement sans restriction, vous permettant de modifier et d\u00e9boguer les applications syst\u00e8me.","unreset-devtools-warning-body":"La r\u00e9initialisation effacera toutes vos donn\u00e9es, y compris les applications que vous avez achet\u00e9es. Aucun de vos param\u00e8tres ni aucune de vos donn\u00e9es ne seront sauvegard\u00e9s. Votre t\u00e9l\u00e9phone ne poss\u00e8dera plus les outils de d\u00e9veloppement sans restriction, vous ne pourrez plus modifier ni d\u00e9boguer les applications syst\u00e8me.","powerSave-explanation":"Le mode \u00e9conomie d\u2019\u00e9nergie d\u00e9sactive la connexion de donn\u00e9es, le Bluetooth et la g\u00e9olocalisation afin de pr\u00e9server la charge de la batterie. Vous pourrez toujours activer \u00e0 nouveau ces services manuellement.","performanceAndUsageDataInfo":{"value":["Aidez-nous \u00e0 am\u00e9liorer ",{"type":"idOrVar","name":"brandShortName"}," en partageant des informations concernant votre t\u00e9l\u00e9phone et la fa\u00e7on dont vous l\u2019utilisez."]},"device-status-connected-device":"Connect\u00e9 \u00e0 l\u2019audio du t\u00e9l\u00e9phone","device-status-connected-device-media":"Connect\u00e9 \u00e0 l\u2019audio du t\u00e9l\u00e9phone/m\u00e9dia","settings":"Param\u00e8tres","advancedSettings":"Param\u00e8tres avanc\u00e9s","default":"D\u00e9faut","enabled":"Activ\u00e9","enable":"Activer","disabled":"D\u00e9sactiv\u00e9","error":"Erreur","ok":"OK","yes":"Oui","no":"Non","reset":"R\u00e9initialiser","factory-reset":"Param\u00e8tres usine","delete":"Supprimer","cancel":"Annuler","close":"Fermer","back":"Retour","continue":"Continuer","skip":"Passer","pair":"Associer","add":"Ajouter","done":"Termin\u00e9","set":"D\u00e9finir","notNow":"Plus tard","turnOn":"Activer","selectASIMCard":"Choisir une carte SIM","sim1":"SIM 1","sim2":"SIM 2","unavailable":"Indisponible","confirmation":"Confirmation","simSettingsWithIndex":{"value":["Param\u00e8tres SIM ",{"type":"idOrVar","name":"index"}]},"simWithIndex":{"value":["SIM ",{"type":"idOrVar","name":"index"}]},"networkAndConnectivity":"R\u00e9seau et connectivit\u00e9","airplaneMode":{"value":"Mode avion","attrs":{"ariaLabel":"Mode avion"}},"geolocation":{"value":"G\u00e9olocalisation","attrs":{"ariaLabel":"G\u00e9olocalisation"}},"wifi":"Wi-Fi","wifi-header":"Wi-Fi","wifi-disabled":"Pour afficher les r\u00e9seaux disponibles, activez le Wi-Fi.","availableNetworks":"R\u00e9seaux disponibles","knownNetworks":"R\u00e9seaux connus","hiddenNetworks":"R\u00e9seaux masqu\u00e9s","scanning":"Recherche\u2026","scanNetworks":"Actualiser","full-status-initializing":"Initialisation\u2026","full-status-connecting":{"value":["Connexion \u00e0 ",{"type":"idOrVar","name":"ssid"},"\u2026"]},"full-status-associated":"Obtention d\u2019une adresse IP\u2026","full-status-connected":{"value":["Connect\u00e9 \u00e0 ",{"type":"idOrVar","name":"ssid"}]},"full-status-connectingfailed":"\u00c9chec de connexion","full-status-disconnected":"Hors connexion","short-status-connecting":"Connexion\u2026","short-status-associated":"Obtention d\u2019une adresse IP\u2026","short-status-connected":"Connect\u00e9","short-status-connectingfailed":"\u00c9chec de connexion","short-status-disconnected":"Hors connexion","wifi-sleep":"Veille Wi-Fi","wifi-sleep-description":"\u00c9teint automatiquement le Wi-Fi lorsque l\u2019appareil est en veille afin de pr\u00e9server la charge de la batterie.","manageNetworks":"G\u00e9rer les r\u00e9seaux","wifi-bad-credentials-title":"La connexion au Wi-Fi a \u00e9chou\u00e9","wifi-bad-credentials-confirm":{"value":["Impossible de se connecter \u00e0 ",{"type":"idOrVar","name":"ssid"},", l\u2019authentification a \u00e9chou\u00e9 \u00e0 cause d\u2019identifiants incorrects. Veuillez saisir \u00e0 nouveau le mot de passe du r\u00e9seau."]},"manageNetworks-header":"G\u00e9rer les r\u00e9seaux","manageCertificates":"G\u00e9rer les certificats","manageCertificates-header":"G\u00e9rer les certificats","joinHiddenNetwork":"Rejoindre un r\u00e9seau masqu\u00e9","securedBy":{"value":["S\u00e9curis\u00e9 par ",{"type":"idOrVar","name":"capabilities"}]},"security":"S\u00e9curit\u00e9","securityNone":"Aucune","securityOpen":"Ouvert","notInRange":"Hors de port\u00e9e","ipAddress":"Adresse IP","linkSpeed":"Vitesse connexion","linkSpeedMbs":{"value":[{"type":"idOrVar","name":"linkSpeed"}," Mbit/s"]},"signalStrength":"Niveau de signal","signalLevel0":"Tr\u00e8s faible","signalLevel1":"Faible","signalLevel2":"Moyen","signalLevel3":"Bon","signalLevel4":"Tr\u00e8s bon","forget":"Oublier","forgetNetwork-dialog":"Oublier ce r\u00e9seau\u202f?","forgetNetwork-confirmation":"Confirmation","authentication":"Authentification","authentication-header":"Authentification","eap":"M\u00e9thode EAP","auth-phase2":"Authentification, \u00e9tape 2","server-certificate":"Certificat du serveur","import-certificate-msg":"Attention\u00a0: si vous devez importer de nouveaux certificats, veuillez vous rendre dans la section \u00ab\u00a0Gestion des certificats\u00a0\u00bb.","identity":"Identifiant","password":"Mot de passe","showPassword":"Afficher le mot de passe","networkNotification":"Notification de r\u00e9seau","networkNotification-expl":"Me signaler quand un r\u00e9seau ouvert est disponible","macAddress":"Adresse MAC","deviceInfo_software":{"value":[{"type":"idOrVar","name":"brandShortName"}," ",{"type":"idOrVar","name":"os"}]},"macUnavailable":"Non disponible","noNetworksFound":"Aucun r\u00e9seau d\u00e9tect\u00e9","noKnownNetworks":"Aucun r\u00e9seau connu","edit":"Modifier","certificateList":"Liste de certificats","noImportedCertificates":"Aucun certificat import\u00e9","deleteCertificate":"Supprimer le certificat","importCertificate":"Importer un certificat","selectCertificateFile":"Choisir fichier de certificat","enterCertificateNickname":"Surnom du certificat","nickname":"Surnom (nick name)","enter-certificate-name-msg":"Vous pouvez modifier le surnom (nick name) ou conserver le nom par d\u00e9faut.","certificate-deletion-failed":"\u00c9chec de la suppression du certificat","certificate-deletion-failed-description":"Un probl\u00e8me a pu se produire avec le stockage, par exemple.","certificate-import-failed":"\u00c9chec de l\u2019importation du certificat","certificate-import-failed-description":"Cela peut venir d\u2019un fichier de certificat endommag\u00e9, par exemple.","wpsMessage":"Se connecter par WPS","wpsMessage-header":"Connexion WPS","fullStatus-wps-inprogress":"WPS en cours\u2026","fullStatus-wps-canceled":"WPS a \u00e9t\u00e9 annul\u00e9","fullStatus-wps-timedout":"WPS expir\u00e9","fullStatus-wps-failed-error":{"value":["WPS a \u00e9chou\u00e9 [",{"type":"idOrVar","name":"error"},"]"]},"fullStatus-wps-overlapped":"Un autre point d\u2019acc\u00e8s WPS PBC a \u00e9t\u00e9 d\u00e9tect\u00e9","wpsDescription2":"Configuration Wi-Fi automatique","wpsMethodSelection":"S\u00e9lectionner une m\u00e9thode WPS\u00a0:","wpsPbcLabel":"Avec le bouton","wpsMyPinLabel":"Avec mon code PIN","wpsApPinLabel":"Avec PIN point d\u2019acc\u00e8s","wpsPinDescription":"Code PIN (4 \u00e0 8 chiffres)","wpsPinInput":{"value":["Saisir ",{"type":"idOrVar","name":"pin"}," sur l\u2019autre appareil"]},"wpsPinAps":"S\u00e9lectionner l\u2019autre appareil","wpsAnyAp":"N\u2019importe lequel","wpsCancelMessage":"Annuler WPS","wpsCancelFailedMessageError":{"value":["\u00c9chec \u00e0 l\u2019annulation de WPS [",{"type":"idOrVar","name":"error"},"]"]},"callSettings":"Param\u00e8tres des appels","callSettings-header":"Param\u00e8tres des appels","callSettingsQuery":"Lecture des informations r\u00e9seau\u2026","callSettingsQueryError":"Erreur dans la requ\u00eate","voiceMail":"Messagerie vocale","voiceMail-header":"Messagerie vocale","voiceMail-number":"Num\u00e9ro de la messagerie","voiceMail-number-notSet":"Non d\u00e9fini","voicePrivacyMode":"Appels chiffr\u00e9s","callWaiting":"Double appel","confirmCallWaitingTitle":"Impossible de confirmer les pr\u00e9f\u00e9rences du double appel","confirmCallWaitingDesc":"L\u2019appareil n\u2019est actuellement pas en mesure de communiquer avec l\u2019op\u00e9rateur. Essayez de d\u00e9finir la pr\u00e9f\u00e9rence \u00e0 nouveau.","callForwarding":"Transfert d\u2019appels","callForwarding-header":"Transfert d\u2019appels","callForwardingForward":"Transf\u00e9rer","callForwardingUnconditional":"Toujours transf\u00e9rer","callForwardingUnconditionalShort":"Toujours","callForwardingMobileBusy":"Transf\u00e9rer si ligne occup\u00e9e","callForwardingMobileBusyShort":"Si ligne occup\u00e9e","callForwardingNoReply":"Transf\u00e9rer si sans r\u00e9ponse","callForwardingNoReplyShort":"Si sans r\u00e9ponse","callForwardingNotReachable":"Transf\u00e9rer si injoignable","callForwardingNotReachableShort":"Si injoignable","callForwardingForwardingVoiceToNumber":{"value":["Appels vocaux vers ",{"type":"idOrVar","name":"number"},""]},"callForwardingNotForwarding":"D\u00e9sactiv\u00e9","callForwardingConfirmTitle":"Message de confirmation du transfert d\u2019appels","cf-alwaysUseThisNumber":"Toujours utiliser ce num\u00e9ro","cf-numberWhenUnreachable":"Num\u00e9ro si injoignable","cf-numberWhenUnanswered":"Num\u00e9ro si sans r\u00e9ponse","cf-numberWhenBusy":"Num\u00e9ro si ligne occup\u00e9e","callForwardingInvalidNumberError":"Ce num\u00e9ro de t\u00e9l\u00e9phone n\u2019est pas valide.","callForwardingSetSuccess":"Op\u00e9ration d\u00e9finie avec succ\u00e8s.","callForwardingSetForbidden":"Votre op\u00e9rateur ne prend pas en charge cette op\u00e9ration.","callForwardingSetError":"Erreur dans les param\u00e8tres d\u2019appel.","callerId":"Pr\u00e9sentation du num\u00e9ro","callBarring":"Blocage d\u2019appels","callBarring-header":"Blocage d\u2019appels","callBarring-outgoing":"Appels sortants","callBarring-baoc":"Tous","callBarring-boic":"\u00c0 l\u2019international","callBarring-boicExhc":"\u00c0 l\u2019international, sauf pays de r\u00e9sidence","callBarring-incoming":"Appels entrants","callBarring-baic":"Tous","callBarring-baicR":"Tous (en itin\u00e9rance)","callBarring-passcode":"Code secret","callBarring-change-passcode":"Changer le code secret","callBarring-passcode-enter":"Saisir le code secret","callBarring-passcode-unknown":"Si vous ne connaissez pas votre code secret, veuillez contacter votre op\u00e9rateur","callBarring-current-passcode":"Code secret actuel","callBarring-new-passcode":"Nouveau code secret","callBarring-create-passcode":"Cr\u00e9er un code secret","callBarring-confirm-passcode":"Confirmer le code secret","callBarring-passcode-error-generic":"Erreur code secret.","callBarring-passcode-error-incorrect":"Le code secret est incorrect.","callBarring-passcode-error-mismatch":"Les codes secrets ne correspondent pas.","callBarring-update-item-error":{"value":["Erreur lors de l\u2019actualisation de l\u2019\u00e9l\u00e9ment\u00a0: ",{"type":"idOrVar","name":"error"},"."]},"callBarring-update-generic-error":"Erreur r\u00e9seau ou code secret erron\u00e9","callBarring-change-passcode-success":"Le code secret a \u00e9t\u00e9 chang\u00e9.","callerId-default":"Valeur par d\u00e9faut du r\u00e9seau","callerId-hide":"Masquer le num\u00e9ro","callerId-show":"Afficher le num\u00e9ro","fdn":"Num\u00e9rotation fixe","fdnSettings":"Num\u00e9rotation fixe FDN","fdnSettings-header":"Num\u00e9rotation fixe (FDN)","fdn-authorizedNumbers":"Num\u00e9ros autoris\u00e9s","fdn-authorizedNumbers-header":"Num\u00e9ros autoris\u00e9s","fdn-authorizedNumbers-desc":"Liste des num\u00e9ros composables","fdn-explanation1":"Restreint les num\u00e9ros qui peuvent \u00eatre compos\u00e9s \u00e0 partir de ce t\u00e9l\u00e9phone.","fdn-explanation2":"(Les num\u00e9ros d\u2019urgence seront toujours composables)","fdnEnable":"Activer FDN","fdnDisable":"D\u00e9sactiver FDN","fdnReset":"R\u00e9initialiser PIN2","fdnAttemptMsg3":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"one":"C\u2019est votre dernier essai autoris\u00e9 pour saisir le code correct avant de bloquer la carte SIM.","two":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant de bloquer la carte SIM."],"few":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant de bloquer la carte SIM."],"many":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant de bloquer la carte SIM."],"other":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant de bloquer la carte SIM."]}},"fdnLastChanceMsg":"C\u2019est votre dernier essai autoris\u00e9 pour saisir le code PIN2 correct. Sinon, vous devrez saisir le code PUK2 pour utiliser cette carte SIM.","fdnErrorMsg":"Le code PIN2 est incorrect.","fdnContact-name":"Nom","fdnContact-number":"Num\u00e9ro","fdnContact-submit":"Valider","fdnAction-add":"Ajouter un contact","fdnAction-call":"Appeler","fdnAction-edit":"Modifier le contact","fdnAction-edit-header":"Modifier le contact","fdnAction-delete":"Supprimer le contact","fdnNoFDNFreeRecord":"Nombre maximum de num\u00e9ros pour la num\u00e9rotation fixe FDN atteint. Vous ne pouvez plus ajouter de num\u00e9ros.","messagingSettings":"Param\u00e8tres des messages","messagingSettings-header":"Param\u00e8tres des messages","message-confirmation":"Confirmation des messages","message-delivery-reports":"Accus\u00e9s de r\u00e9ception","message-delivery-reports-details":"Demander un accus\u00e9 de r\u00e9ception pour chaque message envoy\u00e9","message-read-reports":"Confirmations de lecture","message-read-reports-details":"Demander une confirmation de lecture pour chaque message envoy\u00e9","mmsTitle":"Messages multim\u00e9dias (MMS)","auto-retrieve":"R\u00e9cup\u00e9ration automatique","auto-retrieve-details":"R\u00e9cup\u00e9rer automatiquement les messages","manual-retrieve":"D\u00e9sactiv\u00e9","auto-roaming-retrieve":"Toujours activ\u00e9","auto-no-roaming-retrieve":"Activ\u00e9 sauf en itin\u00e9rance","autoConfigure":"Configuration auto","wapPushMessages":"Messages Push WAP","wapPush":"Push WAP","cellBroadcastMessages":"Messages de diffusion cellulaire","cellBroadcast":"Diffusion cellulaire","emergencyAlert":"Alertes d\u2019urgence","messagingInformation":"Informations","messaging-SMSC":"Centre de service SMS","messaging-sim-settings":{"value":["Param\u00e8tres SIM ",{"type":"idOrVar","name":"index"}]},"unknown-SMSC":"Information non disponible","cellularAndData":"T\u00e9l\u00e9phonie et donn\u00e9es","cellularAndData-header":"T\u00e9l\u00e9phonie et donn\u00e9es","dataOperator":"R\u00e9seau mobile","dataConnectivity":"Donn\u00e9es","dataConnection":"Connexion de donn\u00e9es","dataRoaming":"Itin\u00e9rance de donn\u00e9es","netOperator":"R\u00e9seaux mobiles","netOperator-header":"R\u00e9seaux mobiles","availableOp":"R\u00e9seaux mobiles \u00e0 proximit\u00e9","preferredNetworkTypeAlertTitle":"Type de r\u00e9seau pr\u00e9f\u00e9r\u00e9","preferredNetworkTypeAlertErrorMessage":"Le type de r\u00e9seau ne peut pas \u00eatre enregistr\u00e9","operator-networkType":"Type de r\u00e9seau","operator-networkType-auto":"Automatique","operator-networkType-prefer3G":"3G de pr\u00e9f\u00e9rence","operator-networkType-2G":"2G uniquement","operator-networkType-3G":"3G uniquement","operator-networkType-GSM":"GSM","operator-networkType-WCDMA":"WCDMA","operator-networkType-CDMA":"CDMA","operator-networkType-EVDO":"EVDO","operator-networkType-LTE":"LTE","operator-networkType-preferWCDMA":"WCDMA de pr\u00e9f\u00e9rence","operator-networkType-auto-2G-3G":"Automatique (2G/3G)","operator-networkType-auto-3G-4G":"Automatique (3G/4G)","operator-networkType-auto-CDMA-EVDO":"Automatique (CDMA/EVDO)","operator-networkType-auto-WCDMA-GSM":"Automatique (WCDMA/GSM)","operator-networkType-auto-LTE-WCDMA-GSM":"Automatique (LTE/WCDMA/GSM)","operator-networkType-auto-LTE-WCDMA":"Automatique (LTE/WCDMA)","operator-networkType-auto-LTE-CDMA-EVDO":"Automatique (LTE/CDMA/EVDO)","operator-networkType-auto-WCDMA-GSM-CDMA-EVDO":"Automatique (WCDMA/GSM/CDMA/EVDO)","operator-autoSelect":"S\u00e9lection automatique","operator-networkSelect-auto":"Automatique","operator-networkSelect-manual":"Manuelle","operator-turnAutoSelectionOff":"Pour afficher les r\u00e9seaux mobiles disponibles, d\u00e9sactivez la s\u00e9lection automatique.","operator-info-connecting":"Connexion\u2026","operator-info-failed":"\u00c9chec de connexion","operator-info-connected":"Connect\u00e9","operator-info-available":"Disponible","operator-info-current":"Connect\u00e9","operator-info-forbidden":"Interdit","operator-info-unknown":"\u00c9tat inconnu","operator-roamingPreference":"Pr\u00e9f\u00e9rence d\u2019itin\u00e9rance","operator-roamingPreference-home":"R\u00e9seau op\u00e9rateur","operator-roamingPreference-any":"Tous les r\u00e9seaux","operator-roamingPreference-affiliated":"R\u00e9seaux partenaires","apn":"Nom du point d\u2019acc\u00e8s (APN)","apnSettings":"Param\u00e8tres APN","apnSettings-header":"Param\u00e8tres APN","dataSettings":"Param\u00e8tres donn\u00e9es","dataSettings-header":"Param\u00e8tres donn\u00e9es","suplSettings":"Param\u00e8tres A-GPS","suplSettings-header":"Param\u00e8tres A-GPS","messageSettings":"Param\u00e8tres messages","messageSettings-header":"Param\u00e8tres messages","dunSettings":"Param\u00e8tres mode modem","dunSettings-header":"Param\u00e8tres mode modem","imsSettings":"Param\u00e8tres IMS","imsSettings-header2":"Param\u00e8tres IMS","fotaSettings":"Param\u00e8tres FOTA","fotaSettings-header":"Param\u00e8tres FOTA","apn-editor-header":"\u00c9diteur d\u2019APN","mmsport":"Port MMS","mmsproxy":"Mandataire MMS","mmsc":"MMSC","httpProxyHost":"H\u00f4te mandataire HTTP","httpProxyPort":"Port mandataire HTTP","custom":"(param. personnalis\u00e9s)","apn-authType":"Authentification","apn-authType-notDefined":"Non d\u00e9finie","apn-authType-none":"Aucune","apn-authType-pap":"PAP","apn-authType-chap":"CHAP","apn-authType-papOrChap":"PAP ou CHAP","apn-protocol":"Protocole","apn-roaming-protocol":"Protocole en itin\u00e9rance","apn-protocol-notDefined":"Non d\u00e9fini","apn-protocol-ip":"IPv4","apn-protocol-ipv6":"IPv6","apn-protocol-ipv4v6":"IPv4/IPv6","apn-type":"Type d\u2019APN","apn-mtu":"MTU","dataConnection-warning-head":"Activer la connexion de donn\u00e9es\u202f?","dataConnection-warning-message":"Les applications r\u00e9cup\u00e8reront automatiquement des donn\u00e9es par la connexion de donn\u00e9es quand n\u00e9cessaire. Des frais suppl\u00e9mentaires li\u00e9s aux donn\u00e9es peuvent s\u2019appliquer.","dataConnection-warning-message-short":"Permettre de r\u00e9cup\u00e9rer automatiquement des donn\u00e9es par la connexion de donn\u00e9es.","dataRoaming-warning-head":"Activer l\u2019itin\u00e9rance de donn\u00e9es\u202f?","dataRoaming-warning-message":"Selon votre contrat avec votre op\u00e9rateur, des frais suppl\u00e9mentaires pour vos donn\u00e9es peuvent s\u2019appliquer si vous \u00eates en mode itin\u00e9rance.","dataRoaming-warning-message-short":"Permettre de r\u00e9cup\u00e9rer des donn\u00e9es m\u00eame en itin\u00e9rance.","change-apn-warning-message1":"Selon votre contrat avec votre op\u00e9rateur, modifier vos param\u00e8tres de Nom de point d\u2019acc\u00e8s (APN) peut entra\u00eener des frais suppl\u00e9mentaires en itin\u00e9rance. Voulez-vous continuer\u202f?","reset-apn-warning-title":"R\u00e9initialiser les param\u00e8tres APN","reset-apn-warning-message":"Vos param\u00e8tres seront r\u00e9initialis\u00e9s \u00e0 leur valeur par d\u00e9faut. Cette action est irr\u00e9versible.","state-unknown":"\u00c9tat inconnu","state-available":"Disponible","state-connected":"Connect\u00e9","state-current":"Actuel","state-forbidden":"Interdit","delete-apn-confirm":"Supprimer cet APN\u202f?","delete-apn":"Supprimer","add-apn":"Ajouter un nouvel APN","reset-apn":"R\u00e9initialiser","apnSettings-reset":"R\u00e9initialiser les param\u00e8tres APN","bluetooth":"Bluetooth","bluetooth-header":"Bluetooth","bluetooth-enable-msg":"Pour afficher les appareils \u00e0 proximit\u00e9, activez le Bluetooth.","bluetooth-paired-devices":"Appareils associ\u00e9s","bluetooth-devices-in-area":"Appareils \u00e0 proximit\u00e9","bluetooth-visible-to-all":"Visible par tous","bluetooth-name-maxlength-alert":{"value":["Le nom de votre appareil exc\u00e8de ",{"type":"idOrVar","name":"length"}," caract\u00e8res. Veuillez r\u00e9essayer."]},"bluetooth-address":"Adresse Bluetooth","bluetooth-address-unavailable":"Non disponible","bt-status-nopaired":"Aucun appareil associ\u00e9","bt-status-turnoff":"\u00c9teint","bt-status-paired":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"zero":[{"type":"idOrVar","name":"name"}],"one":[{"type":"idOrVar","name":"name"}," et un autre"],"two":[{"type":"idOrVar","name":"name"}," et ",{"type":"idOrVar","name":"n"}," autres"],"few":[{"type":"idOrVar","name":"name"}," et ",{"type":"idOrVar","name":"n"}," autres"],"many":[{"type":"idOrVar","name":"name"}," et ",{"type":"idOrVar","name":"n"}," autres"],"other":[{"type":"idOrVar","name":"name"}," et ",{"type":"idOrVar","name":"n"}," autres"]}},"device-status-tap-connect":"Appuyez pour \u00e9tablir la connexion","device-status-pairing":"Association avec l\u2019appareil\u2026","device-status-waiting":"En attente de l\u2019autre appareil\u2026","device-status-paired":"Associ\u00e9","device-status-connecting":"Connexion\u2026","device-status-connected":"Connect\u00e9","device-status-connected-media":"Connect\u00e9 \u00e0 l\u2019audio du m\u00e9dia","device-option-unpair":"Dissocier","device-option-confirm":"Confirmer","device-option-unpair-confirmation":"Confirmation","device-option-unpair-device":"Dissocier l\u2019appareil\u202f?","device-option-connect":"Connecter","device-option-disconnect":"D\u00e9connecter","search-for-device":"Recherche d\u2019appareils\u2026","search-device":"Rechercher des appareils","rename-device":"Renommer mon appareil","unnamed-device":"Appareil inconnu","change-btdevice-name":"Changer le nom de l\u2019appareil","change-btdevice-name-desc":"Ce nom appara\u00eetra sur vos autres appareils Bluetooth","bluetooth-new-name":"Nouveau nom","error-pair-title":"Impossible d\u2019associer les appareils","error-pair-pincode":"Impossible de s\u2019associer avec l\u2019appareil. V\u00e9rifiez que le code PIN est correct.","error-pair-toofast":"Impossible de s\u2019associer avec l\u2019appareil. Requ\u00eates trop nombreuses.","error-connect-title":"Impossible de connecter les appareils","error-connect-msg":"V\u00e9rifiez que l\u2019appareil avec lequel vous essayez de vous connecter est toujours \u00e0 port\u00e9e et que le Bluetooth est activ\u00e9.","unpair-title":"Dissocier un appareil connect\u00e9\u202f?","unpair-msg":"Vous \u00eates en ce moment connect\u00e9 avec cet appareil. Le dissocier vous d\u00e9connectera aussi.","unpair-confirm":{"value":[{"type":"idOrVar","name":"unpair-title"},"\\n",{"type":"idOrVar","name":"unpair-msg"}]},"nfc":{"value":"NFC","attrs":{"ariaLabel":"NFC"}},"internetSharing":"Partage de connexion","internetSharing-header":"Partage de connexion","internetSharing-usb":"USB","internetSharing-wifi":"Wi-Fi","wifi-hotspot":"Partage par Wi-Fi","wifi-hotspot-enabled-msg":{"value":["Point d\u2019acc\u00e8s \u00ab\u00a0",{"type":"idOrVar","name":"hotspotSSID"},"\u00a0\u00bb activ\u00e9"]},"wifi-password":"Mot de passe","wifi-security":"S\u00e9curit\u00e9","usb-tethering":"Modem USB","hotspotSettings":"Param\u00e8tres du point d\u2019acc\u00e8s","wifi-auth-username":"Nom d\u2019utilisateur","wifi-auth-password":"Mot de passe","wifi-join-username":"Nom d\u2019utilisateur","wifi-join-password":"Mot de passe","hotspotSettings-header":"Point d\u2019acc\u00e8s","ssid-name":"Nom de r\u00e9seau (SSID)","hotspot-security-open":"Ouvert","hotspot-security-wpa-psk":"WPA","hotspot-security-wpa2-psk":"WPA2","hotspot-password-desc":"Le mot de passe doit contenir au moins 8 caract\u00e8res.","personalization":"Personnalisation","sound":{"value":"Son","attrs":{"ariaLabel":"Son"}},"sound-header":"Son","volume":"Volume","tones":"Sonneries","touch-tones":"Tonalit\u00e9 des touches","touch-tone-short":"Courte","touch-tone-long":"Longue","select-tone":"Choisir sonnerie","ringtones":"Sonneries","alerts":"Alertes","manage-tones":"G\u00e9rer les sonneries","other-sounds":"Autres sons","unlock-screen":"D\u00e9verrouillage \u00e9cran","camera":"Appareil photo","sent-message":"Message envoy\u00e9","alarm-sound":"Alarmes","ringtone-and-notification-sound":"Sonneries et notifications","media-sound":"M\u00e9dias","saving-tone":"Enregistrement\u2026","no-ringtone-app":"Aucune application sonnerie trouv\u00e9e pour g\u00e9rer les sonneries.","display":{"value":"Affichage","attrs":{"ariaLabel":"Affichage"}},"display-header":"Affichage","brightness":"Luminosit\u00e9","brightness-autoAdjust":"Ajustement automatique","lockScreen":"\u00c9cran de verrouillage","wallpaper":"Fond d\u2019\u00e9cran","screen-timeout":"D\u00e9lai de l\u2019\u00e9cran de veille","one-minute":"1 minute","two-minutes":"2 minutes","five-minutes":"5 minutes","ten-minutes":"10 minutes","screen-timeout-never":"Jamais","lock-orientation":"Emp\u00eacher la rotation","navigation":{"value":"Navigation","attrs":{"ariaLabel":"Navigation"}},"navigation-header":"Navigation","edge-swipe-navigation":"Navigation lat\u00e9rale","edge-swipe-navigation-enable-msg":"Faites glisser votre doigt depuis le bord droit ou gauche de l\u2019\u00e9cran pour passer d\u2019une application \u00e0 l\u2019autre.","notifications":{"value":"Notifications","attrs":{"ariaLabel":"Notifications"}},"notifications-header":"Notifications","lockscreen-notifications":"Sur l\u2019\u00e9cran de verrouillage","notifications-resend":"Au red\u00e9marrage","dateAndTime":"Date et heure","dateAndTime-header":"Date et heure","setTimeAutomatically":"R\u00e9glage automatique","timezoneMessage":"Fuseau horaire","dateMessage":"Date","timeMessage":"Heure","selectTimezoneContinent":"Choisir un continent","selectTimezone":"Choisir un fuseau horaire","time-format":"Format","time-format-time":"Format d\u2019affichage de l\u2019heure","useDefaultTimeFormat":"Par d\u00e9faut","hour-12":"12 heures","hour-24":"24 heures","languageAndRegion":"Langue et r\u00e9gion","language":"Langue","language-header":"Langue","region":"R\u00e9gion","more-languages":"Ajouter d\u2019autres langues","sample-format":"Aper\u00e7u du format","longDateFormat":"%A %d %Eb %Y","shortDateFormat":"%d %Eb %Y","simManager":"Gestionnaire de SIM","simManager-header":"G\u00e9rer les cartes SIM","simSettings":"Param\u00e8tres SIM","sim-manager-outgoing-calls":"Appels sortants","sim-manager-outgoing-call-desc":"Les appels seront \u00e9mis depuis","sim-manager-outgoing-messages":"Messages sortants","sim-manager-outgoing-messages-desc":"Les SMS seront envoy\u00e9s depuis","sim-manager-outgoing-data":"Donn\u00e9es","sim-manager-outgoing-data-description":"Service de donn\u00e9es mobiles et paiements du Marketplace","sim-manager-always-ask":"Toujours demander","sim-pin-locked":"Code PIN verrouill\u00e9","no-operator":"Aucun op\u00e9rateur","cant-disable-simcard-alert":"Au moins une carte SIM doit \u00eatre active","disable-simcard-confirm":{"value":["La carte SIM ",{"type":"idOrVar","name":"disableCardIndex"}," ne peut recevoir aucun appel ni aucun message. Les appels et messages sortants configur\u00e9s pour utiliser la carte SIM ",{"type":"idOrVar","name":"disableCardIndex"}," seront impossibles."]},"change-outgoing-data-confirm":"Le transfert de donn\u00e9es en cours sera suspendu. Continuer quand m\u00eame\u202f?","home-screens":{"value":"\u00c9crans d\u2019accueil","attrs":{"ariaLabel":"\u00c9crans d\u2019accueil"}},"homescreen-layout":"Disposition","homescreen-icon-layout":"Disposition des ic\u00f4nes","homescreen-three-columns":"Trois colonnes","homescreen-four-columns":"Quatre colonnes","notice":"Avertissement","search":{"value":"Recherche","attrs":{"ariaLabel":"Recherche"}},"search-header":"Recherche","search-engine":"Moteur de recherche","search-suggestions":"Activer les suggestions","search-suggestions-description":"Les caract\u00e8res que vous saisissez lorsque vous effectuez une recherche sont envoy\u00e9s \u00e0 plusieurs moteurs de recherche et seront utilis\u00e9s pour vous retourner des suggestions de recherche. Des frais suppl\u00e9mentaires li\u00e9s aux donn\u00e9es peuvent s\u2019appliquer lors de l\u2019utilisation de cette fonctionnalit\u00e9.","replaceable-home-screen":"\u00c9cran d\u2019accueil rempla\u00e7able","change-home-screen":"Changer l\u2019\u00e9cran d\u2019accueil","get-more-home-screens":"Obtenir plus d\u2019\u00e9crans d\u2019accueil","my-home-screens":"Mes \u00e9crans d\u2019accueil","home-screen-description":"Description","delete-home-screen-button":"Supprimer l\u2019\u00e9cran d\u2019accueil","keyboardSettings":"Param\u00e8tres du clavier","selectedKeyboards":"Claviers s\u00e9lectionn\u00e9s","keyboards":"Claviers","keyboards-header":"Claviers","numberKeyboardLayouts":"Dispositions de chiffres","optionKeyboardLayouts":"Dispositions d\u2019options","keypad":"Clavier num\u00e9rique","dialpad":"Clavier t\u00e9l\u00e9phonique","selectKeyboards":"S\u00e9lectionner des claviers","selectKeyboards-header":"S\u00e9lectionner des claviers","mustHaveOneKeyboard":{"value":["Au moins un clavier de type ",{"type":"idOrVar","name":"type"}," doit \u00eatre s\u00e9lectionn\u00e9"]},"keyboardType-text":"texte","keyboardType-url":"url","keyboardType-number":"chiffres","defaultKeyboardEnabled":{"value":[{"type":"idOrVar","name":"appName"},"\u00a0: ",{"type":"idOrVar","name":"layoutName"}," est d\u00e9fini comme m\u00e9thode de saisie par d\u00e9faut"]},"keyboard-layout-emoji":"\u00c9motic\u00f4nes","themes":{"value":"Th\u00e8mes","attrs":{"ariaLabel":"Th\u00e8mes"}},"themes-header":"Th\u00e8mes","fxa":"Comptes Firefox","accounts":"Gestion des comptes","fxa-accounts":"Comptes Firefox","fxa-accounts-header":"Comptes Firefox","fxa-overview":"Votre compte Firefox vous permet d\u2019utiliser des fonctionnalit\u00e9s comme Localiser mon appareil et des applications comme le Firefox Marketplace.","fxa-invitation":"Cr\u00e9er un compte ou se connecter","fxa-confirm-account":"Confirmer le compte","fxa-verification-email-sent-msg":{"value":["Nous allons envoyer un message \u00e0\u00a0: ",{"type":"idOrVar","name":"email"}]},"fxa-please-verify":"Veuillez cliquer sur le lien de v\u00e9rification du courriel pour confirmer votre compte.","fxa-cancel-confirmation":"Annuler la confirmation du compte","fxa-resend-alert":{"value":["Un lien de v\u00e9rification a \u00e9t\u00e9 envoy\u00e9 \u00e0 ",{"type":"idOrVar","name":"email"},"."]},"fxa-dont-see-email-resend":"Vous n\u2019avez rien re\u00e7u\u202f? Renvoyer.","fxa-about":"\u00c0 propos","fxa-logged-in-text":{"value":["Connect\u00e9 en tant que ",{"type":"idOrVar","name":"email"}]},"fxa-options":"Options","fxa-sign-out":"Se d\u00e9connecter","fxa-confirm-email":{"value":["Veuillez confirmer ",{"type":"idOrVar","name":"email"}]},"findmydevice":"Localiser mon appareil","findmydevice-create-account":"Cr\u00e9er un compte ou se connecter","findmydevice-enable":"Localiser mon appareil","findmydevice-about":"\u00c0 propos de Localiser mon appareil","findmydevice-enable-geolocation-prompt":"Activer la g\u00e9olocalisation\u202f?","findmydevice-enable-geolocation-description":"Pour afficher la position de votre appareil, vous devez activer la g\u00e9olocalisation.","findmydevice-geolocation-enabled":"G\u00e9olocalisation activ\u00e9e","findmydevice-enable-network":"Cet appareil est actuellement hors connexion. Veuillez vous connecter \u00e0 un r\u00e9seau puis r\u00e9essayer.","findmydevice-error":"Une erreur s\u2019est produite dans Localiser mon appareil.","findmydevice-please-try":"Veuillez r\u00e9essayer.","findmydevice-please-tap":"Veuilez appuyer pour r\u00e9essayer.","fxsync-overview":"Connectez-vous pour synchroniser votre historique, vos mots de passe et bien plus","fxsync-get-started":"Commencer","fxsync-sync-user":"Compte Firefox","fxsync-disable":"D\u00e9sactiver Firefox Sync","fxsync-last-synced":{"value":["Derni\u00e8re synchronisation ",{"type":"idOrVar","name":"when"}]},"fxsync-collections":"Collections","fxsync-legal":"Mentions l\u00e9gales","privacyAndSecurity":"Vie priv\u00e9e et s\u00e9curit\u00e9","passcode-lock":"Verrouillage par code","passcode-lock-desc":{"value":["Code d\u2019acc\u00e8s\u00a0: ",{"type":"idOrVar","name":"code"}]},"screenLock":"Verrouillage","screenLock-header":"Verrouillage","screenLock-enabled-with-no-passcode":"Activ\u00e9 sans code d\u2019acc\u00e8s","screenLock-enabled-with-passcode":"Activ\u00e9 avec code d\u2019acc\u00e8s","simSecurity":"S\u00e9curit\u00e9 carte SIM","simSecurity-header":"S\u00e9curit\u00e9 carte SIM","appPermissions":{"value":"Permissions applications","attrs":{"ariaLabel":"Permissions applications"}},"appPermissions-header":"Permissions applications","author":"Auteur","permissions":"Permissions","revoke":"R\u00e9voquer","ask":"Demander","block":"Bloquer","allow2":"Autoriser","uninstallApp":"D\u00e9sinstaller l\u2019application","phone":"T\u00e9l\u00e9phone","messages":"Messages","vibrate":"Vibreur","ring":"Sonnerie","change":"Changer","create":"Cr\u00e9er","create-a-passcode":"Cr\u00e9er un code d\u2019acc\u00e8s","passcode":"Code d\u2019acc\u00e8s","passcode-heading":"Code d\u2019acc\u00e8s","enter-passcode":"Saisir le code d\u2019acc\u00e8s","new-passcode":"Nouveau code","current-passcode":"Code d\u2019acc\u00e8s actuel","incorrect-passcode":"Le code d\u2019acc\u00e8s est incorrect.","passcode-doesnt-match":"Les codes d\u2019acc\u00e8s ne correspondent pas. Veuillez r\u00e9essayer.","confirm-passcode":"Confirmer le code d\u2019acc\u00e8s","immediately":"Imm\u00e9diatement","after-one-minute":"Au bout d\u2019une minute","after-five-minutes":"Au bout de 5 minutes","after-fifteen-minutes":"Au bout de 15 minutes","after-thirty-minutes":"Au bout de 30 minutes","after-one-hour":"Au bout d\u2019une heure","require-passcode":"Demander le code d\u2019acc\u00e8s","change-passcode":"Changer le code d\u2019acc\u00e8s","noSimCard":"Aucune carte SIM","simPin":"Code PIN carte SIM","simPin2":"Code PIN2 carte SIM","simPinWithIndex":{"value":["Code PIN carte SIM ",{"type":"idOrVar","name":"index"}]},"pukCode":"Code PUK","puk2Code":"Code PUK2","whatIsSimPin":"Qu\u2019est-ce qu\u2019un code PIN\u202f?","simPinIntro1":"Le code PIN de la carte SIM emp\u00eache l\u2019acc\u00e8s aux donn\u00e9es r\u00e9seau de votre carte SIM. Quand il est activ\u00e9, tout appareil \u00e9quip\u00e9 de cette carte SIM r\u00e9clamera le code PIN au d\u00e9marrage.","simPinIntro2":"Un code PIN de carte SIM est diff\u00e9rent du code destin\u00e9 \u00e0 d\u00e9verrouiller l\u2019appareil.","changeSimPin":"Modifier le code PIN","simPinChangedSuccessfully":"Code PIN carte SIM correctement modifi\u00e9","simPinChangedSuccessfullyWithIndex":{"value":["Code PIN carte SIM ",{"type":"idOrVar","name":"index"}," correctement modifi\u00e9"]},"pinTitle":"Saisir le code PIN","pin2Title":"Saisir le code PIN2","pukTitle":"Saisir le code PUK","puk2Title":"Saisir le code PUK2","newpinTitle":"Nouveau code PIN","pinErrorMsg":"Le code PIN est incorrect.","inputCodeRetriesLeft":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"one":"Dernier essai.","two":[{"type":"idOrVar","name":"n"}," essais restants."],"few":[{"type":"idOrVar","name":"n"}," essais restants."],"many":[{"type":"idOrVar","name":"n"}," essais restants."],"other":[{"type":"idOrVar","name":"n"}," essais restants."]}},"pinAttemptMsg3":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"one":"C\u2019est votre dernier essai autoris\u00e9 pour saisir le code correct avant de bloquer la carte SIM.","two":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant de bloquer la carte SIM."],"few":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant de bloquer la carte SIM."],"many":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant de bloquer la carte SIM."],"other":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant de bloquer la carte SIM."]}},"pinLastChanceMsg":"C\u2019est votre dernier essai autoris\u00e9 pour saisir le code PIN correct. Sinon, vous devrez saisir le code PUK pour utiliser cette carte SIM.","simCardLockedMsg":"La carte SIM est verrouill\u00e9e.","simCardBlockedMsg":"La carte SIM est bloqu\u00e9e.","enterPukMsg":"Vous devez saisir la Personal Unlocking Key (PUK, soit Cl\u00e9 personnelle de d\u00e9blocage) pour la carte SIM. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation associ\u00e9e \u00e0 votre carte SIM ou contacter votre op\u00e9rateur pour en savoir plus.","enterPuk2Msg":"Vous devez saisir la Personal Unlocking Key (PUK2, soit Cl\u00e9 personnelle de d\u00e9blocage) pour la carte SIM. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation associ\u00e9e \u00e0 votre carte SIM ou contacter votre op\u00e9rateur pour en savoir plus.","pukErrorMsg":"Le code PUK est incorrect.","pukAttemptMsg3":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"one":"C\u2019est votre dernier essai autoris\u00e9 pour saisir le code correct avant que cette carte SIM ne devienne d\u00e9finitivement inutilisable. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation associ\u00e9e \u00e0 votre carte SIM ou contacter votre op\u00e9rateur pour en savoir plus.","two":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant que cette carte SIM ne devienne d\u00e9finitivement inutilisable. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation associ\u00e9e \u00e0 votre carte SIM ou contacter votre op\u00e9rateur pour en savoir plus."],"few":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant que cette carte SIM ne devienne d\u00e9finitivement inutilisable. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation associ\u00e9e \u00e0 votre carte SIM ou contacter votre op\u00e9rateur pour en savoir plus."],"many":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant que cette carte SIM ne devienne d\u00e9finitivement inutilisable. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation associ\u00e9e \u00e0 votre carte SIM ou contacter votre op\u00e9rateur pour en savoir plus."],"other":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant que cette carte SIM ne devienne d\u00e9finitivement inutilisable. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation associ\u00e9e \u00e0 votre carte SIM ou contacter votre op\u00e9rateur pour en savoir plus."]}},"pukLastChanceMsg":"C\u2019est votre dernier essai autoris\u00e9 pour saisir le code PUK correct. Votre carte SIM deviendra d\u00e9finitivement inutilisable si vous saisissez un code PUK incorrect. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation associ\u00e9e \u00e0 votre carte SIM ou contacter votre op\u00e9rateur pour en savoir plus.","newSimPinMsg":"Nouveau code PIN (4 \u00e0 8 chiffres)","newSimPin2Msg":"Nouveau code PIN2 (4 \u00e0 8 chiffres)","confirmNewSimPinMsg":"Confirmer le nouveau code PIN","confirmNewSimPin2Msg":"Confirmer le nouveau code PIN2","newPinErrorMsg":"Les codes PIN ne correspondent pas","simLockedPhone":"T\u00e9l\u00e9phone verrouill\u00e9 sur ce r\u00e9seau","unknownSimCardState":"\u00c9tat carte SIM inconnu","simCardNotReady":"Carte SIM pas encore pr\u00eate","simCardIllegal":"Carte SIM non autoris\u00e9e","genericLockError":"Une erreur inconnue s\u2019est produite","doNotTrack":"Ne pas me pister","doNotTrack-header":"Ne pas me pister","doNotTrackActions":"Indiquer aux sites et applications que je ne souhaite pas \u00eatre pist\u00e9","allowTracking":"Indiquer aux sites et applications que je souhaite \u00eatre pist\u00e9","doNotHavePrefOnTracking":"Ne rien indiquer aux sites et applications concernant mes pr\u00e9f\u00e9rences de pistage","doNotTrack-dt":"Principe de fonctionnement","doNotTrack-dd1":"Lorsque vous activez Ne pas me pister, votre appareil informe tous les sites web et applications (aussi bien les publicitaires que les autres fournisseurs de contenu) que vous ne souhaitez pas que votre comportement soit pist\u00e9.","doNotTrack-dd2":"Activer cette fonctionnalit\u00e9 n\u2019affectera en rien votre capacit\u00e9 \u00e0 vous identifier sur des sites web, ni ne fera oublier \u00e0 votre appareil vos informations personnelles, telles que le contenu de vos paniers d\u2019achats, vos informations de g\u00e9olocalisation, ou vos informations d\u2019identification.","browsingPrivacy":{"value":"Confidentialit\u00e9 navigation","attrs":{"ariaLabel":"Confidentialit\u00e9 navigation"}},"browsingPrivacy-header":"Confidentialit\u00e9 navigation","confirm-clear-history-title":"Effacer l\u2019historique","confirm-clear-history-desc":"Voulez-vous vraiment effacer votre historique\u202f? Dans le cas o\u00f9 vous utilisez votre compte Firefox, l\u2019historique synchronis\u00e9 sera \u00e9galement effac\u00e9.","confirm-clear-cookies-cache-title":"Effacer les cookies et le cache","confirm-clear-cookies-cache-desc":"Voulez-vous vraiment effacer les cookies et le cache\u202f?","clear":"Effacer","privateBrowsing":"Navigation priv\u00e9e","privateBrowsingDetails":"Ouvrir tous les liens externes dans une nouvelle fen\u00eatre priv\u00e9e.","trackingProtection":"Protection contre le pistage","trackingProtectionDetails":"Prot\u00e9ger votre navigation contre les \u00e9l\u00e9ments de pistage potentiels dans toutes les nouvelles fen\u00eatres.","browser-privacy-history-header":"Historique","browser-privacy-history-desc":"Effacer l\u2019historique de navigation de cet appareil. Dans le cas o\u00f9 vous utilisez votre compte Firefox, l\u2019historique synchronis\u00e9 sera \u00e9galement effac\u00e9.","browser-privacy-history-button":"Effacer l\u2019historique","browser-privacy-cookies-cache-header":"Cookies et cache","browser-privacy-cookies-cache-desc":"Effacer les donn\u00e9es stock\u00e9es (comme les cookies) de cet appareil. Les informations d\u2019identification et les pr\u00e9f\u00e9rences seront perdues.","browser-privacy-cookies-cache-button":"Effacer les cookies et le cache","storage":"Stockage","enableUSBStorage1":"Stockage USB","umsUnplugToDisable":"D\u00e9brancher pour d\u00e9sactiver","appStorage":"Stockage des applications","appStorage-header":"Stockage des applications","storageSize":{"value":[{"type":"idOrVar","name":"size"},"\u00a0",{"type":"idOrVar","name":"unit"}]},"availableSize":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"size"}]}],"value":{"zero":[{"type":"idOrVar","name":"size"},"\u00a0",{"type":"idOrVar","name":"unit"}," disponible"],"one":[{"type":"idOrVar","name":"size"},"\u00a0",{"type":"idOrVar","name":"unit"}," disponible"],"two":[{"type":"idOrVar","name":"size"},"\u00a0",{"type":"idOrVar","name":"unit"}," disponibles"],"few":[{"type":"idOrVar","name":"size"},"\u00a0",{"type":"idOrVar","name":"unit"}," disponibles"],"many":[{"type":"idOrVar","name":"size"},"\u00a0",{"type":"idOrVar","name":"unit"}," disponibles"],"other":[{"type":"idOrVar","name":"size"},"\u00a0",{"type":"idOrVar","name":"unit"}," disponibles"]}},"apps-total-space":"Espace total","apps-used-space":"Utilis\u00e9","apps-free-space":"Libre","storage-details":"Les photos, vid\u00e9os et la musique sont stock\u00e9es \u00e0 un emplacement diff\u00e9rent de celui des applications. Voir Stockage des m\u00e9dias pour plus de d\u00e9tails.","ums-shared-volumes":{"value":["Partage de\u00a0: ",{"type":"idOrVar","name":"list"}]},"transferProtocol-header":"Protocole de transfert","protocol-mtp":"Protocole de transfert m\u00e9dia (MTP)","protocol-ums":"Stockage USB de masse (UMS)","mediaStorage":"Stockage des m\u00e9dias","mediaStorage-header":"Stockage des m\u00e9dias","storage-name-internal":"Stockage interne","storage-name-external-0":"Stockage carte SD","storage-name-external-1":"Stockage carte SD 2","short-storage-name-internal":"Interne","short-storage-name-external-0":"Carte SD","short-storage-name-external-1":"Carte SD 2","music-space":"Musique","pictures-space":"Photos","videos-space":"Vid\u00e9os","left-space":"Espace libre","free-space":"Espace libre","total-space":"Espace total","size-not-available":"Indisponible","no-storage":"Aucun support de stockage d\u00e9tect\u00e9","advanced":"Avanc\u00e9","ums-warning-title":"Activer le stockage USB de masse\u202f?","default-media-location":"Emplacement des m\u00e9dias","default-media-location-msg":"Choisir o\u00f9 sont stock\u00e9s par d\u00e9faut les photos, vid\u00e9os, la musique et les t\u00e9l\u00e9chargements.","change-default-media-location-confirmation":"Changer le volume de stockage par d\u00e9faut des m\u00e9dias affectera l\u2019emplacement o\u00f9 seront enregistr\u00e9s par d\u00e9faut les m\u00e9dias. Les donn\u00e9es existantes resteront \u00e0 leur emplacement actuel. Voulez-vous continuer\u202f?","eject-sdcard-external-0":"\u00c9jecter la carte SD","eject-sdcard-external-1":"\u00c9jecter la carte SD 2","format-sdcard-internal":"Formater le stockage interne","format-sdcard-external-0":"Formater la carte SD","format-sdcard-external-1":"Formater la carte SD 2","formatting":"Formatage\u2026","unmount-sdcard-confirmation":"Vous ne pourrez plus acc\u00e9der au contenu stock\u00e9 sur la carte SD apr\u00e8s l\u2019avoir \u00e9ject\u00e9e. Voulez-vous continuer\u202f?","format-sdcard-title":"Formater la carte SD","format-sdcard-message":"Toutes les donn\u00e9es stock\u00e9es sur votre carte SD seront effac\u00e9es, comme par exemple les photos ou la musique.","format-sdcard-internal-title":"Formater le stockage interne","format-sdcard-internal-message":"Toutes les donn\u00e9es stock\u00e9es sur le stockage interne seront effac\u00e9es, comme par exemple les photos ou la musique.","format-sdcard-btnformat":"Formater","sdcardUnmounted":"Carte SD \u00e9ject\u00e9e","ums-partial-support-warning-title":"Attention\u00a0: le stockage USB de masse peut ne pas fonctionner","ums-partial-support-warning-msg-1":"Le stockage USB de masse ne fonctionne qu\u2019avec une carte SD externe. Si vous souhaitez acc\u00e9der \u00e0 la fois \u00e0 votre espace de stockage interne et externe, vous devez utiliser MTP.","ums-partial-support-warning-msg-2":"Choisissez \u00ab\u00a0OK\u00a0\u00bb pour activer le stockage USB de masse, ou \u00ab\u00a0Annuler\u00a0\u00bb pour rester sur le mode MTP.","device":"Appareil","help":{"value":"Aide","attrs":{"ariaLabel":"Aide"}},"deviceInfo":{"value":"Informations","attrs":{"ariaLabel":"Informations"}},"deviceInfo-header":"Informations","deviceInfo-IMEI":"IMEI","deviceInfo-IMEI-with-index":{"value":["IMEI ",{"type":"idOrVar","name":"index"},"\u00a0: ",{"type":"idOrVar","name":"imei"},""]},"deviceInfo-ICCID":"ICCID","deviceInfo-ICCID-with-index":{"value":["SIM ",{"type":"idOrVar","name":"index"},"\u00a0: ",{"type":"idOrVar","name":"iccid"},""]},"deviceInfo-MSISDN":"Num\u00e9ro de t\u00e9l\u00e9phone","deviceInfo-MSISDN-with-index":{"value":["SIM ",{"type":"idOrVar","name":"index"},"\u00a0: ",{"type":"idOrVar","name":"msisdn"},""]},"deviceInfo-ICCID-unavailable-sim":{"value":["SIM ",{"type":"idOrVar","name":"index"},"\u00a0: indisponible"]},"downloads":{"value":"T\u00e9l\u00e9chargements","attrs":{"ariaLabel":"T\u00e9l\u00e9chargements"}},"downloads-header":"T\u00e9l\u00e9chargements","unknown-phoneNumber":"Inconnu","unknown-phoneNumber-sim":{"value":["SIM ",{"type":"idOrVar","name":"index"},"\u00a0: inconnu"]},"gitInfo":"Informations Git","model-name":"Mod\u00e8le","os-version":"Version du syst\u00e8me","firmware_revision":"R\u00e9vision du micrologiciel","hardware_manufacturer":"Constructeur du mat\u00e9riel","hardware_platform":"Plate-forme du mat\u00e9riel","hardware_revision":"R\u00e9vision du mat\u00e9riel","platform_version":"Version de la plate-forme","build-id":"Identifiant de version","update-channel":"Canal de mise \u00e0 jour","build_number":"Num\u00e9ro de compilation","update-url":"Adresse de mise \u00e0 jour","last-updated":"Derni\u00e8re mise \u00e0 jour","more-info":"Plus d\u2019informations","more-info-header":"Plus d\u2019informations","softwareUpdates":"Mises \u00e0 jour logicielles","check-update-daily":"Chaque jour","check-update-weekly":"Chaque semaine","check-update-monthly":"Chaque mois","check-for-updates":"Rechercher des mises \u00e0 jour","check-update-now":"Rechercher maintenant","update-status":"\u00c9tat des mises \u00e0 jour","no-network-when-update":"Veuillez vous connecter \u00e0 Internet pour poursuivre la mise \u00e0 jour.","checking-for-update":"Recherche de mises \u00e0 jour\u2026","no-updates":"Aucune mise \u00e0 jour disponible","retry-when-online":"Le r\u00e9seau est d\u00e9connect\u00e9. La recherche s\u2019effectuera une fois connect\u00e9 au r\u00e9seau.","already-latest-version":{"value":["Vous poss\u00e9dez d\u00e9j\u00e0 la derni\u00e8re version de ",{"type":"idOrVar","name":"brandShortName"}]},"check-error":"Une erreur s\u2019est produite lors de la recherche de mises \u00e0 jour.","aboutBrowserOS":{"value":["\u00c0 propos de ",{"type":"idOrVar","name":"brandShortName"}]},"browser-os-desc":{"value":[{"type":"idOrVar","name":"brandShortName"}," est le syst\u00e8me d\u2019exploitation libre et ouvert de Mozilla. Notre mission est de promouvoir l\u2019ouverture et l\u2019innovation, et de faire en sorte que la puissance du Web reste entre vos mains."]},"learn-more":"En savoir plus","reset-head":"R\u00e9initialisation","reset-warning-title":"ATTENTION","addons-auto-update":"Mise \u00e0 jour automatique des modules","addons-update-notify":"M\u2019en informer apr\u00e8s l\u2019installation","your-rights":"Vos droits","your-rights-header":"Vos droits","about-your-rights-0":{"value":[{"type":"idOrVar","name":"brandShortName"}," repose sur un logiciel libre et ouvert r\u00e9alis\u00e9 par une communaut\u00e9 de milliers de contributeurs dans le monde. Voici ce que vous devriez savoir\u00a0:"]},"about-your-rights-1":{"value":[{"type":"idOrVar","name":"brandShortName"}," est fourni selon les termes de plusieurs licences libres dont la Mozilla Public License. Un appareil ",{"type":"idOrVar","name":"brandShortName"}," peut \u00e9galement contenir des logiciels propri\u00e9taires de tierces parties. Tout code fourni sous une licence libre vous donne le droit de le modifier et de distribuer vos versions modifi\u00e9es \u00e0 condition de respecter les termes de sa licence."]},"about-your-rights-2":{"value":["Il ne vous est c\u00e9d\u00e9 aucun droit ou licence sur les marques d\u00e9pos\u00e9es de la Fondation Mozilla ou de tiers, y compris et sans limitation, sur le nom et le logo de ",{"type":"idOrVar","name":"brandShortName"},"."]},"about-your-rights-3":{"value":["Certaines fonctionnalit\u00e9s de ",{"type":"idOrVar","name":"brandShortName"},", comme le rapporteur de plantages, vous permettent de fournir un retour \u00e0 Mozilla. En choisissant d\u2019envoyer ces informations, vous donnez \u00e0 Mozilla la permission d\u2019utiliser ce retour pour am\u00e9liorer ses logiciels, de le publier sur ses sites web et de le distribuer."]},"about-legal-info":"Informations l\u00e9gales","about-credit-info":"Contributeurs","about-legal-info-header":"Informations l\u00e9gales","open-source-notices":"Mentions l\u00e9gales open source","open-source-notices-header":"Mentions l\u00e9gales","obtaining-source-code":"Obtenir le code source","obtaining-source-code-header":"Obtenir le code source","about-mozilla":"Mozilla","about-other":"Autre","your-privacy":"Votre vie priv\u00e9e","your-privacy-header":"Votre vie priv\u00e9e","licensing-1":"Les copies ex\u00e9cutables de ce logiciel sont fournies par le projet Mozilla sous la Mozilla Public License 2.0 (MPL). D\u00e9couvrez vos droits.","licensing-2":"La totalit\u00e9 du code source de ce logiciel est disponible sous des licences libres et open source. La plus grande partie en est disponible sous la licence Mozilla Public License 2.0 (MPL).","licensing-3":"Les autres parties du logiciel qui ne sont pas disponibles selon la licence Mozilla Public License 2.0 MPL sont disponibles selon diff\u00e9rentes autres licences. Celles qui imposent la reproduction du texte de licence sont reprises ci-dessous. (Note\u00a0: votre copie de ce logiciel peut ne pas contenir de code couvert par certaines des licences list\u00e9es ici, cela d\u00e9pend de la version pr\u00e9cise du logiciel que vous avez choisie.)","licensing-MPL":"Mozilla Public License 2.0","licensing-Apache":"Apache License 2.0","developer":{"value":"D\u00e9veloppeurs","attrs":{"ariaLabel":"D\u00e9veloppeurs"}},"developer-header":"D\u00e9veloppeurs","hud-settings-button":"Informations d\u00e9veloppeurs","hud-settings-header":"Informations d\u00e9veloppeurs","hud-devtools":"Outils de d\u00e9veloppement","hud-devtools-header":"Outils de d\u00e9veloppement","hud-hide":"Cacher tous les \u00e9l\u00e9ments HUD","hud-logging":"Changements dans ADB","hud-system":"Informations syst\u00e8me","hud-problems":"Probl\u00e8mes","hud-warnings":"Avertissements","hud-errors":"Erreurs","hud-security":"Probl\u00e8mes de s\u00e9curit\u00e9","hud-performance":"Performances","hud-reflows":"Reflows","hud-jank":"Blocages","hud-jank-threshold":"Seuil de blocage (ms)","hud-reflows-threshold":"Dur\u00e9e du seuil (ms)","hud-fps":"Images par seconde","hud-ttl":"Dur\u00e9e de chargement","hud-memory":"M\u00e9moire","hud-uss":"Unique Set Size","hud-appmemory":"M\u00e9moire application","hud-jsobjects":"Objets JS","hud-jsstrings":"Cha\u00eenes JS","hud-jsother":"JS (autre)","hud-dom":"DOM","hud-style":"Style","hud-other":"Autres","software":"Logiciel","devSettings":"Param\u00e8tres pour les d\u00e9veloppeurs","debug":"D\u00e9bogage","developer-menu":"Menu d\u00e9veloppeurs","developer-tools":"Outils de d\u00e9veloppement","graphics":"Acc\u00e9l\u00e9ration graphique","hardware-composer":"Composition mat\u00e9rielle","paint-flashing":"Zones repeintes","dumps-layers":"Informations arbres de calques","dumps-display-list":"Informations display list","dumps-layerscope":"Informations layerscope","apz-overscroll":"Overscrolling","tiling":"Tuilage","low-precision":"Painting basse pr\u00e9cision","low-opacity":"Transparence basse pr\u00e9cision","draw-tile-borders":"Dessiner bords du tuilage","draw-layer-borders":"Dessiner bords du calque","log-animations":"Enregistrer les animations lentes","remote-debugging-usb":"D\u00e9bogage USB","devtools-wifi":"Outils de d\u00e9veloppement par Wi-Fi","devtools-device-name":"Nom pour les outils de d\u00e9v. Wi-Fi","devtools-pseudolocalization":"Pseudo-langues","debugger-disabled":"D\u00e9sactiv\u00e9","debugger-adb-only":"ADB uniquement","debugger-adb-and-devtools":"ADB et outils de d\u00e9veloppement","wifi-debugging":"Sortie Wi-Fi dans ADB","bluetooth-debugging":"Sortie Bluetooth dans ADB","bluetooth-snoop-debugging":"Espionnage Bluetooth HCI sur la carte SD","nfc-debugging":"Sortie NFC dans ADB","ril-debugging":"Sortie RIL dans ADB","network-debugging":"Sortie r\u00e9seau dans ADB","mms-debugging":"Sortie MMS dans ADB","console-enabled":"Activer la console","cards-view-screenshots":"Miniatures\u00a0: captures d\u2019\u00e9cran","geolocation-debugging-enabled":"Sortie g\u00e9olocalisation dans ADB","geolocation-debugging-gps-locations-ignored":"Ignorer les positions GPS","use-reviewer-certs":"Certificats relecteur Marketplace","shake-to-log":"Secouer pour enregistrer les journaux syst\u00e8me","enable-logshake-qa":"Activer le mode QA LogShake","launch-ftu":"Tutoriel de prise en main","no-ftu":"Aucun tutoriel de prise en main trouv\u00e9","software-home-button":"\u00c9muler le bouton Accueil","home-gesture":"Geste de retour \u00e0 l\u2019accueil","continuous-transition":"Transition continue","debug-gaia-enabled":"Traces de d\u00e9bogage de Gaia","app-transition":"Transition d\u2019application","app-suspending":"Suspension d\u2019application","copypaste":"Copier-coller","multi-screen":"Multi-\u00e9crans","verbose-app-permissions":"Permissions d\u2019applications d\u00e9taill\u00e9es","pinning-the-web":"Mode \u00c9pingler le Web","view-source":"Activer le geste pour afficher le code source","homescreen-paging":"Activer la pagination de l\u2019\u00e9cran d\u2019accueil","window-management":"Gestion de la fen\u00eatre","unlock-privileges":"D\u00e9verrouillage des privil\u00e8ges","full-devtools":"Outils de d\u00e9veloppement non restreints","reset-devtools-warning-title":"ATTENTION","service-worker":"Service workers","service-worker-scope":"Port\u00e9e","service-worker-script-spec":"Script spec","service-worker-current-worker-url":"Adresse URL du worker courant","service-worker-active-cache-name":"Nom du cache actuel","service-worker-waiting-cache-name":"Nom du cache en attente","service-worker-update":"Mise \u00e0 jour","service-worker-unregister":"D\u00e9senregistrement","service-worker-no-registrations":"Aucun service worker trouv\u00e9","developer-menu-disabled-warning-title":"Menu d\u00e9veloppeur non actif","developer-menu-disabled-warning-msg-1":"Le mode d\u00e9veloppeur ne peut \u00eatre activ\u00e9 si le menu d\u00e9veloppeur n\u2019est pas d\u2019abord activ\u00e9.","developer-menu-disabled-warning-msg-2":"Merci de l\u2019activer depuis Param\u00e8tres > Informations > Plus d\u2019informations > Menu d\u00e9veloppeur.","enable-full-dev-mode-first-warning-title":"Voulez-vous activer le mode d\u00e9veloppeur\u202f?","enable-full-dev-mode-first-warning-msg-1":"CETTE ACTION D\u00c9TRUIRA TOUTES VOS DONN\u00c9ES ET EFFECTUERA UNE REMISE \u00c0 Z\u00c9RO COMPL\u00c8TE.","enable-full-dev-mode-first-warning-msg-2":"CETTE ACTION D\u00c9SACTIVERA TOUTES LES S\u00c9CURIT\u00c9S SUR VOTRE APPAREIL.","enable-full-dev-mode-final-warning-title":"Voulez-vous activer le mode d\u00e9veloppeur\u202f?","enable-full-dev-mode-final-warning-msg":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"count"}]}],"value":{"one":["Appuyez sur \u00ab\u00a0Activer\u00a0\u00bb ",{"type":"idOrVar","name":"count"}," fois de plus pour continuer."],"two":["Appuyez sur \u00ab\u00a0Activer\u00a0\u00bb ",{"type":"idOrVar","name":"count"}," fois de plus pour continuer."],"few":["Appuyez sur \u00ab\u00a0Activer\u00a0\u00bb ",{"type":"idOrVar","name":"count"}," fois de plus pour continuer."],"many":["Appuyez sur \u00ab\u00a0Activer\u00a0\u00bb ",{"type":"idOrVar","name":"count"}," fois de plus pour continuer."],"other":["Appuyez sur \u00ab\u00a0Activer\u00a0\u00bb ",{"type":"idOrVar","name":"count"}," fois de plus pour continuer."]}},"downloads-edit":"Mode \u00e9dition","downloads-select-all":"Tout s\u00e9lectionner","downloads-deselect-all":"Tout d\u00e9s\u00e9lectionner","downloads-delete":"Supprimer","battery":"Batterie","battery-header":"Batterie","battery-level-percent":{"value":[{"type":"idOrVar","name":"level"},"\u00a0%"]},"battery-current-level":{"value":["Niveau actuel (",{"type":"idOrVar","name":"level"},"\u00a0%)"]},"battery-charging-calculating":"Calcul de la dur\u00e9e de charge\u2026","battery-discharging-calculating":"Calcul du temps restant\u2026","battery-charging-complete":"Charge compl\u00e8te","battery-discharging-complete":"Batterie \u00e9puis\u00e9e","battery-time-hour":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"hours"}]}],"value":{"zero":[{"type":"idOrVar","name":"hours"},"\u00a0h"],"one":[{"type":"idOrVar","name":"hours"},"\u00a0h"],"two":[{"type":"idOrVar","name":"hours"},"\u00a0h"],"few":[{"type":"idOrVar","name":"hours"},"\u00a0h"],"many":[{"type":"idOrVar","name":"hours"},"\u00a0h"],"other":[{"type":"idOrVar","name":"hours"},"\u00a0h"]}},"battery-time-minute":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"minutes"}]}],"value":{"zero":[{"type":"idOrVar","name":"minutes"},"\u00a0min"],"one":[{"type":"idOrVar","name":"minutes"},"\u00a0min"],"two":[{"type":"idOrVar","name":"minutes"},"\u00a0min"],"few":[{"type":"idOrVar","name":"minutes"},"\u00a0min"],"many":[{"type":"idOrVar","name":"minutes"},"\u00a0min"],"other":[{"type":"idOrVar","name":"minutes"},"\u00a0min"]}},"battery-charging-minutes":{"value":[{"type":"idOrVar","name":"battery-time-minute"}," avant charge compl\u00e8te"]},"battery-charging-hours":{"value":[{"type":"idOrVar","name":"battery-time-hour"}," ",{"type":"idOrVar","name":"battery-time-minute"}," avant charge compl\u00e8te"]},"battery-discharging-minutes":{"value":["Batterie \u00e9puis\u00e9e dans ",{"type":"idOrVar","name":"battery-time-minute"}]},"battery-discharging-hours":{"value":["Batterie \u00e9puis\u00e9e dans ",{"type":"idOrVar","name":"battery-time-hour"}," ",{"type":"idOrVar","name":"battery-time-minute"}]},"powerSaveModeTitle":"Mode \u00e9conomie d\u2019\u00e9nergie","powerSaveMode":"\u00c9conomie d\u2019\u00e9nergie","powerSave-threshold":{"value":["\u00c0 ",{"type":"idOrVar","name":"level"},"\u00a0% restants"]},"powerSave-never":"Jamais","turnOnAuto":"Activer automatiquement","accessibility":{"value":"Accessibilit\u00e9","attrs":{"ariaLabel":"Accessibilit\u00e9"}},"accessibility-header":"Accessibilit\u00e9","screenReader":"Lecteur d\u2019\u00e9cran","screenReader-header":"Lecteur d\u2019\u00e9cran","screenReader-toggle":"Lecteur d\u2019\u00e9cran","colorFilter":"Filtres de couleurs","colorFilter-header":"Filtres de couleurs","colorFilter-toggle":"Filtres de couleurs","accessibility-audio":"Audio","accessibility-audio-monoAudio":"Audio mono","mono-enabled":"Mono activ\u00e9","mono-disabled":"Mono d\u00e9sactiv\u00e9","screenReader-captions":"Afficher des sous-titres","screenReader-enable-msg":"Lorsque le lecteur d\u2019\u00e9cran est activ\u00e9, votre appareil fournira un retour vocal pour guider les utilisateurs aveugles et malvoyants.","screen-reader-speech-volume":"Volume de lecture","screen-reader-speech-rate":"Vitesse de lecture","screenReader-confirm-title-start":"Activer le lecteur d\u2019\u00e9cran\u202f?","screenReader-confirm-description-start":"Activer le lecteur d\u2019\u00e9cran modifiera le fonctionnement de l\u2019\u00e9cran tactile. Appuyez \u00e0 n\u2019importe quel endroit pour entendre l\u2019\u00e9l\u00e9ment situ\u00e9 sous votre doigt, et appuyez deux fois pour l\u2019activer.","screenReader-confirm-button-start":"Activer","screenReader-confirm-title-stop":"D\u00e9sactiver le lecteur d\u2019\u00e9cran\u202f?","screenReader-confirm-description-stop":"D\u00e9sactiver le lecteur d\u2019\u00e9cran arr\u00eatera tout retour vocal.","screenReader-confirm-button-stop":"D\u00e9sactiver","colorFilter-invert":"Inverser","colorFilter-grayscale":"Nuances de gris","colorFilter-contrast":"Contraste","accessibility-input":"Modes de saisie","accessibility-input-desc":"Ajustements pour les gestes et le tactile","tap-hold-delay":"D\u00e9lai de l\u2019appui long","short-hold-delay":"Court","medium-hold-delay":"Moyen","long-hold-delay":"Long","improveBrowserOS":{"value":["Am\u00e9liorer ",{"type":"idOrVar","name":"brandShortName"}],"attrs":{"ariaLabel":{"value":["Am\u00e9liorer ",{"type":"idOrVar","name":"brandShortName"}]}}},"improveBrowserOSTitle":{"value":["Am\u00e9liorer ",{"type":"idOrVar","name":"brandShortName"}]},"sendMozillaFeedback":"Donner votre avis \u00e0 Mozilla","performanceAndUsageData":"Donn\u00e9es performances et utilisation","sharePerformanceAndUsageData":"Envoyer les donn\u00e9es","appUsageByRegion":"Utilisation des applications","crashReports":"Rapports de plantage","crashReports-header":"Rapports de plantage","crashReportInfo":"Envoyer un rapport \u00e0 Mozilla lorsqu\u2019un plantage se produit nous permet de r\u00e9soudre le probl\u00e8me pour tout le monde. Les rapports sont envoy\u00e9s uniquement par Wi-Fi.","alwaysSendReport":"Toujours envoyer","neverSendReport":"Ne jamais envoyer","askToSendReport2":"Toujours demander","crash-reports-description-1":"Un rapport de plantage contient des d\u00e9tails concernant le plantage et votre appareil, ainsi qu\u2019un instantan\u00e9 de l\u2019\u00e9tat de votre appareil lors du plantage.","crash-reports-description-2":"Il peut inclure diff\u00e9rentes choses comme les applications et pages ouvertes, le texte saisi dans les formulaires et le contenu des messages ouverts, l\u2019historique de navigation r\u00e9cent, ou la g\u00e9olocalisation utilis\u00e9e par une application ouverte.","crash-reports-description-3-start":"Nous utilisons les rapports de plantage pour essayer de r\u00e9soudre les probl\u00e8mes et am\u00e9liorer nos logiciels. Nous g\u00e9rons vos informations comme d\u00e9crit dans notre ","crash-reports-description-3-privacy":"politique de confidentialit\u00e9","crash-reports-description-3-end":".","sendFeedbackTitle":"Donner votre avis","feedback-description":"Votre avis nous aidera \u00e0 am\u00e9liorer Firefox\u00a0OS.","feedback-happy":"Satisfait","feedback-sad":"D\u00e9\u00e7u","feedback-mood-description":"Quel est votre sentiment\u202f?","feedback_whyfeel_happy":"Vous \u00eates satisfait\u202f?","feedback_whyfeel_sad":"Vous \u00eates d\u00e9\u00e7u\u202f?","feedback-mood-explain":"Pouvez-vous nous expliquer en d\u00e9tail\u202f?","feedback-public-description":"Votre avis sera rendu public, assurez-vous donc de ne pas inclure d\u2019informations personnelles.","feedback-contactyou":"Cochez cette case pour nous permettre de vous contacter par courriel.","email-address1":"Adresse \u00e9lectronique\u00a0:","email-keepprivate":"Pas d\u2019inqui\u00e9tude, votre adresse \u00e9lectronique restera confidentielle.","feedback-sendbtn":"Envoyer mon avis","feedback-errormessage-connect-error":"Le syst\u00e8me ne peut pas envoyer votre avis car la connexion a \u00e9chou\u00e9. Veuillez corriger le probl\u00e8me de connexion et l\u2019envoyer \u00e0 nouveau.","feedback-errormessage-server-off":"Le syst\u00e8me ne peut pas envoyer votre avis lors de la phase d\u2019arr\u00eat du serveur. Veuillez patienter un instant et l\u2019envoyer \u00e0 nouveau.","feedback-errormessage-timeout":"Le syst\u00e8me ne peut pas envoyer votre avis car le d\u00e9lai d\u2019attente de la connexion est d\u00e9pass\u00e9. Veuillez red\u00e9marrer et l\u2019envoyer \u00e0 nouveau.","feedback-errormessage-wrong-email":"Le syst\u00e8me ne peut pas envoyer votre avis car l\u2019adresse \u00e9lectronique n\u2019est pas renseign\u00e9e ou son format est incorrect. Veuillez renseigner une adresse \u00e9lectronique correcte et l\u2019envoyer \u00e0 nouveau.","feedback-errormessage-empty-comment":"Le syst\u00e8me ne peut pas envoyer votre avis car le champ commentaire est vide. Veuillez renseigner le formulaire et l\u2019envoyer \u00e0 nouveau.","feedback-complete-msg":"Merci d\u2019avoir donn\u00e9 votre avis\u202f! Nous l\u2019utiliserons pour am\u00e9liorer les prochaines versions de Firefox\u00a0OS.","feedback-errormessage-unknown-error":"Une erreur est survenue. Merci de r\u00e9essayer.","online-support":"Aide en ligne\u00a0:","call-support":"Aide par t\u00e9l\u00e9phone\u00a0:","user-guide":"Manuel de l\u2019utilisateur","default-support":"Pour obtenir de l\u2019aide, veuillez contacter votre revendeur.","simToolkit-header":"Bo\u00eete \u00e0 outils SIM","operatorServices":"Services de l\u2019op\u00e9rateur","operatorServices-help":"Aide","operatorServices-helpmenu":"Menu d\u2019aide","stkAppsNotAvailable":"Applications de la carte SIM indisponibles.","stkItemsNaiSetUpCall":"Configurer appels","stkItemsNaiSendSs":"Envoyer SS","stkItemsNaiSendUssd":"Envoyer USSD","stkItemsNaiSendSms":"Envoyer SMS","stkItemsNaiSendDtmf":"Envoyer DTMF","stkItemsNaiLaunchBrowser":"Lancer le navigateur","stkItemsNaiPlayTone":"\u00c9couter sonneries","stkItemsNaiDisplayText":"Afficher texte","stkItemsNaiGetInkey":"Effectuer une saisie","stkItemsNaiGetInput":"Effectuer une saisie","stkItemsNaiSelectItem":"S\u00e9lectionner \u00e9l\u00e9ment","stkItemsNaiSetUpMenu":"Configurer menu","stkItemsNaiProvideLocalInfo":"Fournir des informations locales","stkItemsNaiSetIdleModeText":"D\u00e9finir texte quand inactif","stkItemsNaiPerformCardApdu":"Lancer l\u2019APDU de la carte","stkItemsNaiPowerOnCard":"Activer la carte","stkItemsNaiPowerOffCard":"D\u00e9sactiver la carte","stkItemsNaiGetReaderStatus":"Obtenir l\u2019\u00e9tat du lecteur","stkItemsNaiOpenChannel":"Ouvrir canal","stkItemsNaiCloseChannel":"Fermer canal","stkItemsNaiReceiveData":"Recevoir donn\u00e9es","stkItemsNaiSendData":"Envoyer donn\u00e9es","stkItemsNaiGetChannelStatus":"Obtenir l\u2019\u00e9tat du canal","stkItemsNaiEndOfTheProactiveSession":"Fin de la session proactive","stkEntry":{"attrs":{"ariaLabel":{"value":[{"type":"idOrVar","name":"title"}]}}},"is-warning-storage-header":"Stockage USB","is-warning-tethering-header":"Modem USB","is-warning-wifi-header":"Point d\u2019acc\u00e8s Wi-Fi","is-warning-storage-tethering-message":"Activer le stockage USB d\u00e9sactivera le mode modem USB. Voulez-vous activer le stockage USB\u202f?","is-warning-tethering-storage-message":"Activer le mode modem USB d\u00e9sactivera le stockage USB. Voulez-vous activer le mode modem USB\u202f?","is-warning-tethering-wifi-message":"Activer le mode modem USB d\u00e9sactivera le point d\u2019acc\u00e8s Wi-Fi. Voulez-vous activer le mode modem USB\u202f?","is-warning-wifi-tethering-message":"Activer le point d\u2019acc\u00e8s Wi-Fi d\u00e9sactivera le mode modem USB. Voulez-vous activer le point d\u2019acc\u00e8s Wi-Fi\u202f?","settings-dialog-alert":"Alerte","settings-dialog-confirm":"Confirmation","settings-dialog-prompt":"Invite","settings-alert-dialog-header":{"value":[{"type":"idOrVar","name":"settings-dialog-alert"}]},"settings-confirm-dialog-header":{"value":[{"type":"idOrVar","name":"settings-dialog-confirm"}]},"settings-prompt-dialog-header":{"value":[{"type":"idOrVar","name":"settings-dialog-prompt"}]},"accessibility-app-icon":{"attrs":{"ariaLabel":{"value":["Application ",{"type":"idOrVar","name":"appName"}]}}},"addon":"Module","addons":{"value":"Modules compl\u00e9mentaires","attrs":{"ariaLabel":"Modules compl\u00e9mentaires"}},"addons-header":"Modules compl\u00e9mentaires","addon-details-header1":{"value":[{"type":"idOrVar","name":"appName"}]},"addon-enable":"Activer le module","addon-description-header":"Description","addon-developer-name":{"value":[{"type":"idOrVar","name":"developerName"}]},"addon-targets":"Applications modifi\u00e9es","addon-options":"Options","addon-delete":"Supprimer le module","addon-share":"Partager le module","addon-share-no-provider":"Pas de partage d\u2019application possible","addon-no-description":"Ce module ne contient pas de description dans son fichier manifeste.","addon-no-targets":"Ce module ne modifie aucune application install\u00e9e.","addon-restart-apps-to-disable":"Module d\u00e9sactiv\u00e9\u00a0; red\u00e9marrer les applications modifi\u00e9es.","addon-reboot-required":"Ce module modifie des applications syst\u00e8me\u00a0; vous devez red\u00e9marrer votre appareil pour le d\u00e9sactiver compl\u00e8tement.","addon-rename":"Renommer","addon-rename-input":"Nom du module","addon-rename-desc":"Changer le nom de votre module","addon-obsolete":"Ce module est obsol\u00e8te et ne fonctionne pas.","addon-obsolete-can-delete":"Ce module est obsol\u00e8te et ne fonctionne pas. Veuillez le supprimer.","my-addons":"Mes modules","get-more-addons":"Installer d\u2019autres modules","achievements":{"value":"Badges","attrs":{"ariaLabel":"Badges"}},"achievements-customize-champ-name":"Champion de la personnalisation","achievements-customize-champ-description":"Ouvrez le Customizer dans une application","achievements-add-on-all-star-name":"Cr\u00e9ateur aguerri","achievements-add-on-all-star-description":"Utilisez le Customizer pour cr\u00e9er votre propre module","achievements-fab-finder-name":"Explorateur curieux","achievements-fab-finder-description":"Installez une application ou un module depuis le Hackerplace","achievements-content-commander-name":"Commandant du contenu","achievements-content-commander-description":"Envoyez une application ou un module sur le Hackerplace","achievements-sharing-is-caring-name":"Le partage c\u2019est la sant\u00e9","achievements-sharing-is-caring-description":"Partagez vos applications et modules avec l\u2019application Partage","achievements-creative-kind-name":"Esprit cr\u00e9atif","achievements-creative-kind-description":"Utilisez l\u2019\u00e9diteur de th\u00e8mes pour cr\u00e9er un nouveau th\u00e8me","service-workers-interception":"Intercepter les requ\u00eates","service-workers-testing":"Activer sur HTTP","closeButton":{"attrs":{"ariaLabel":"Fermer"}},"changeWallpaperButton":{"attrs":{"ariaLabel":"Changer le fond d\u2019\u00e9cran"}},"editButton":{"attrs":{"ariaLabel":"Modifier"}},"wifiSection":{"attrs":{"ariaLabel":"Wi-Fi"}},"simManagerSection":{"attrs":{"ariaLabel":"Gestionnaire de SIM"}},"callSettingsSection":{"attrs":{"ariaLabel":"Param\u00e8tres des appels"}},"messagingSettingsSection":{"attrs":{"ariaLabel":"Param\u00e8tres des messages"}},"cellularAndDataSection":{"attrs":{"ariaLabel":"T\u00e9l\u00e9phonie et donn\u00e9es"}},"bluetoothSection":{"attrs":{"ariaLabel":"Bluetooth"}},"internetSharingSection":{"attrs":{"ariaLabel":"Partage de connexion"}},"dateAndTimeSection":{"attrs":{"ariaLabel":"Date et heure"}},"languageSection":{"attrs":{"ariaLabel":"Langue"}},"keyboardsSection":{"attrs":{"ariaLabel":"Claviers"}},"fxa-accountsSection":{"attrs":{"ariaLabel":"Comptes Firefox"}},"findmydeviceSection":{"attrs":{"ariaLabel":"Localiser mon appareil"}},"fxsyncSection":{"attrs":{"ariaLabel":"Firefox Sync"}},"screenLockSection":{"attrs":{"ariaLabel":"Verrouillage"}},"simSecuritySection":{"attrs":{"ariaLabel":"S\u00e9curit\u00e9 carte SIM"}},"doNotTrackSection":{"attrs":{"ariaLabel":"Ne pas me pister"}},"enableStorageSection":{"attrs":{"ariaLabel":"Stockage USB"}},"mediaStorageSection":{"attrs":{"ariaLabel":"Stockage des m\u00e9dias"}},"appStorageSection":{"attrs":{"ariaLabel":"Stockage des applications"}},"batterySection":{"attrs":{"ariaLabel":"Batterie"}},"helpImprove":"Aidez-nous \u00e0 am\u00e9liorer nos logiciels et services en envoyant automatiquement des donn\u00e9es statistiques. Des frais suppl\u00e9mentaires li\u00e9s aux donn\u00e9es peuvent s\u2019appliquer.","learn-more-privacy":"Lisez notre politique de confidentialit\u00e9","basicMetricsRadio":"Envoi basique","enhancedMetricsRadio":"Envoi am\u00e9lior\u00e9","noneMetricsRadio":"Aucun envoi","basicMetricsExplain":"Envoyer l\u2019utilisation des applications et les rapports d\u2019erreurs.","enhancedMetricsExplain":"Envoyer les rapports basiques plus des informations de diagnostic.","noneMetricsExplain":"Ne pas envoyer de donn\u00e9es.","fxsync":"Firefox Sync","fxsync-collections-bookmarks":"Marque-pages","fxsync-collections-history":"Historique","fxsync-collections-passwords":"Mots de passe","fxsync-empty-account":"Ce compte est vide. Vous devez d\u2019abord lancer la synchronisation \u00e0 partir de Firefox, Firefox pour Android ou Firefox pour iOS puis r\u00e9essayer ici.","fxsync-error-invalid-account":"Ce compte est inactif.","fxsync-error-invalid-account-explanation":"Vous devez d\u2019abord activer Firefox Sync sur un ordinateur ou un appareil Android puis r\u00e9essayer.","fxsync-error-no-key-fetch-token":"Veuillez r\u00e9essayer de vous connecter.","fxsync-error-no-key-fetch-token-explanation":"Nous avons d\u00e9tect\u00e9 une erreur au sein de la session actuelle de votre compte Firefox.","fxsync-error-offline":"Aucune connexion r\u00e9seau.","fxsync-error-offline-explanation":"Vous devez \u00eatre en ligne pour utiliser Firefox Sync.","fxsync-error-unknown":"Oups, une erreur s\u2019est produite.","fxsync-error-unknown-explanation":"Nous nous excusons pour la g\u00eane occasionn\u00e9e. Veuillez r\u00e9essayer plus tard.","fxsync-tos":"Conditions d\u2019utilisation","fxsync-privacy":"Politique de confidentialit\u00e9","fxsync-signing":"Connexion\u2026","fxsync-sync-now":"Synchroniser maintenant","fxsync-syncing":"Synchronisation en cours","ringtone-none":"Aucune","ringer_bubble":"Dragon","ringer_chimer":"Partage","ringer_cunning":"Bleu","ringer_dulco":"Ambiance asiatique","ringer_firefox2":{"value":[{"type":"idOrVar","name":"ringtoneBrandName"}]},"ringer_garden":"Croissance","ringer_marbled":"Ramune","ringer_opti":"Transformation","ringer_pecko":"Pivert","ringer_pingy":"T\u00e9l\u00e9portation","ringer_poppop":"Alerte vaisseau spatial","ringer_sata":"Lien","ringer_simple":"Anthropique","ringer_standard1":"Hyperbole","ringer_standard2":"Malice","ringer_stutter":"Phoniatrie","ringer_symmetry":"Autrui","ringer_tailwind":"Afrique","ringer_thoughtful":"R\u00eave","ringer_wit":"Raison","ringer_zen":"Verrillon","notifier_bells":"Campaniforme","notifier_bloom":"Enfance","notifier_chime":"Tatouage","notifier_clicka":"Fontaine de bambou","notifier_digi":"Arc","notifier_dropplet":"Goutte d\u2019eau","notifier_firefox2":{"value":[{"type":"idOrVar","name":"ringtoneBrandName"}]},"notifier_hello":"Nouveau message","notifier_lowmarble":"\u00c9cho","notifier_ping":"Sonar","notifier_plickle":"Idiophone","notifier_pocko":"Abeille","notifier_quado":"Lipogramme","notifier_red":"\u00c9volution","notifier_sato":"Aux fronti\u00e8res du r\u00e9el","notifier_swipo":"Luxure","notifier_trickleup":"R\u00e9sonance","notifier_trink":"Saccharification","notifier_verse":"Bille","tz-region":"R\u00e9gion","tz-city":"Ville","tzRegion-Africa":"Afrique","tzRegion-America":"Am\u00e9rique","tzRegion-Antarctica":"Antarctique","tzRegion-Asia":"Asie","tzRegion-Atlantic":"Oc\u00e9an Atlantique","tzRegion-Australia":"Australie","tzRegion-Europe":"Europe","tzRegion-Indian":"Oc\u00e9an Indien","tzRegion-Pacific":"Oc\u00e9an Pacifique","title-app":"Permission d\u2019application","title-web":"Permission de site web","title-cam":"Choix de la cam\u00e9ra","perm-alarms":"Programmer des alarmes","perm-audio-capture":"Microphone","perm-audio-capture-appRequest":{"value":["Voulez-vous partager votre microphone avec l\u2019application ",{"type":"idOrVar","name":"app"},"\u202f?"]},"perm-audio-capture-webRequest":{"value":["Voulez-vous partager votre microphone avec ",{"type":"idOrVar","name":"site"},"\u202f?"]},"perm-audio-channel-content":"Lire des sons","perm-audio-channel-notification":"Lire notifications sonores","perm-audio-channel-ringer":"Lire des sonneries","perm-background-sensors":"Donn\u00e9es du capteur","perm-bluetooth":"Bluetooth","perm-browser":"Inspecter des pages web","perm-camera":"Cam\u00e9ras","perm-camera-selector-appRequest":{"value":["Veuillez s\u00e9lectionner une cam\u00e9ra pour l\u2019application ",{"type":"idOrVar","name":"app"},"."]},"perm-camera-selector-webRequest":{"value":["Veuillez s\u00e9lectionner une cam\u00e9ra pour ",{"type":"idOrVar","name":"site"},"."]},"perm-camera-appRequest":{"value":["L\u2019application ",{"type":"idOrVar","name":"app"}," souhaite utiliser votre cam\u00e9ra."]},"perm-camera-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite utiliser votre cam\u00e9ra."]},"perm-cellbroadcast":"Diffusions cellulaires","perm-contacts":"Contacts","perm-contacts-appRequest":{"value":["L\u2019application ",{"type":"idOrVar","name":"app"}," souhaite acc\u00e9der \u00e0 votre liste de contacts."]},"perm-contacts-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite acc\u00e9der \u00e0 votre liste de contacts."]},"perm-desktop-notification":"Notifications","perm-desktop-notification-appRequest":{"value":["L\u2019application ",{"type":"idOrVar","name":"app"}," souhaite afficher des notifications."]},"perm-desktop-notification-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite afficher des notifications."]},"perm-device-storage-apps":"Stockage applications","perm-device-storage-music":"Stockage musique","perm-device-storage-music-appRequest":{"value":["L\u2019application ",{"type":"idOrVar","name":"app"}," souhaite acc\u00e9der \u00e0 votre musique."]},"perm-device-storage-music-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite acc\u00e9der \u00e0 votre musique."]},"perm-device-storage-pictures":"Stockage images","perm-device-storage-pictures-appRequest":{"value":["L\u2019application ",{"type":"idOrVar","name":"app"}," souhaite acc\u00e9der \u00e0 vos images."]},"perm-device-storage-pictures-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite acc\u00e9der \u00e0 vos images."]},"perm-device-storage-sdcard":"Stockage carte m\u00e9moire","perm-device-storage-sdcard-appRequest":{"value":["L\u2019application ",{"type":"idOrVar","name":"app"}," souhaite acc\u00e9der \u00e0 votre carte m\u00e9moire."]},"perm-device-storage-sdcard-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite acc\u00e9der \u00e0 votre carte m\u00e9moire."]},"perm-device-storage-videos":"Stockage vid\u00e9os","perm-device-storage-videos-appRequest":{"value":["L\u2019application ",{"type":"idOrVar","name":"app"}," souhaite acc\u00e9der \u00e0 vos vid\u00e9os."]},"perm-device-storage-videos-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite acc\u00e9der \u00e0 vos vid\u00e9os."]},"perm-embed-apps":"Embarquer des applications","perm-fmradio":"Radio FM","perm-fmradio-appRequest":{"value":["L\u2019application ",{"type":"idOrVar","name":"app"}," souhaite utiliser la radio FM."]},"perm-fmradio-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite utiliser la radio FM."]},"perm-geolocation":"G\u00e9olocalisation","perm-geolocation-appRequest":{"value":["L\u2019application ",{"type":"idOrVar","name":"app"}," souhaite conna\u00eetre votre position."]},"perm-geolocation-more-info":{"value":[{"type":"idOrVar","name":"brandShortName"}," utilise les donn\u00e9es du GPS, du Wi-Fi et des antennes de t\u00e9l\u00e9phonie mobile environnantes pour d\u00e9terminer approximativement votre position. Les donn\u00e9es sont envoy\u00e9es \u00e0 Mozilla ainsi qu\u2019\u00e0 d\u2019autres fournisseurs de services et sont mises en commun afin d\u2019am\u00e9liorer la pr\u00e9cision des positions. Les donn\u00e9es peuvent \u00eatre stock\u00e9es sur votre appareil et des donn\u00e9es peuvent \u00eatre collect\u00e9es m\u00eame lorsqu\u2019aucune application n\u2019est ex\u00e9cut\u00e9e."]},"perm-geolocation-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite conna\u00eetre votre position."]},"perm-idle":"Consulter l\u2019\u00e9tat de l\u2019inactivit\u00e9","perm-indexedDB-chrome-settings":"IndexedDB en mode privil\u00e9gi\u00e9","perm-indexedDB-unlimited":"Stockage IndexedDB illimit\u00e9","perm-input-manage":"G\u00e9rer les claviers","perm-input":"Comportement d\u2019un clavier","perm-media-capture":"Cam\u00e9ra et microphone","perm-media-capture-appRequest":{"value":["Voulez-vous partager votre cam\u00e9ra et votre microphone avec l\u2019application ",{"type":"idOrVar","name":"app"},"\u202f?"]},"perm-media-capture-webRequest":{"value":["Voulez-vous partager votre cam\u00e9ra et votre microphone avec ",{"type":"idOrVar","name":"site"},"\u202f?"]},"perm-mobileconnection":"Connexion mobile","perm-mobileid":"Identit\u00e9 mobile","perm-network-events":"\u00c9v\u00e8nements r\u00e9seau","perm-open-remote-window":"Lancer une application","perm-permissions":"Changer des permissions","perm-power":"Param\u00e8tres d\u2019\u00e9nergie","perm-settings":"Changer des param\u00e8tres","perm-systemXHR":"Requ\u00eates cross-origin","perm-telephony":"T\u00e9l\u00e9phonie","perm-video-capture":"Cam\u00e9ra","perm-video-capture-appRequest":{"value":["Voulez-vous partager votre cam\u00e9ra avec l\u2019application ",{"type":"idOrVar","name":"app"},"\u202f?"]},"perm-video-capture-webRequest":{"value":["Voulez-vous partager votre cam\u00e9ra avec ",{"type":"idOrVar","name":"site"},"\u202f?"]},"perm-voicemail":"Contr\u00f4ler messagerie vocale","perm-webapps-manage":"Installer applications web","perm-wifi-manage":"G\u00e9rer le Wi-Fi","perm-attention":"Ouvrir des pop-up","perm-audio-channel-alarm":"Lire les sonneries d\u2019alarme","perm-audio-channel-normal":"Lire des sons","perm-audio-channel-publicnotification":"Lire notifications publiques","perm-audio-channel-telephony":"Lire sonneries t\u00e9l\u00e9phoniques","perm-backgroundservice":"Service d\u2019arri\u00e8re-plan","perm-device-storage-crashes":"Stocker donn\u00e9es de plantage","perm-downloads":"Acc\u00e9der aux t\u00e9l\u00e9chargements","perm-embed-widgets":"Embarquer des widgets","perm-feature-detection":"D\u00e9tecter les fonctionnalit\u00e9s","perm-firefox-accounts":"Comptes Firefox","perm-mobilenetwork":"R\u00e9seau mobile","perm-moz-attention":"Ouvrir des pop-up (moz)","perm-moz-audio-channel-ringer":"Lire des sonneries (moz)","perm-moz-audio-channel-telephony":"Lire sonneries t\u00e9l\u00e9ph. (moz)","perm-moz-firefox-accounts":"Comptes Firefox (moz)","perm-networkstats-manage":"Statistiques consommation","perm-nfc":"NFC","perm-nfc-hci-events":"\u00c9v\u00e8nements NFC HCI","perm-nfc-manager":"G\u00e9rer le NFC","perm-phonenumberservice":"Service num\u00e9ro de t\u00e9l\u00e9phone","perm-push":"API Push","perm-resourcestats-manage":"Statistiques de ressources","perm-sms":"Lire et envoyer des SMS","perm-speaker-control":"Contr\u00f4ler le volume","perm-storage":"Acc\u00e9der au stockage","perm-tcp-socket":"Sockets TCP","perm-test-permission":"Permission test","perm-time":"R\u00e9gler la date et l\u2019heure","perm-udp-socket":"Sockets UDP","perm-wappush":"Push WAP","weekday-0-long":"Dimanche","weekday-1-long":"Lundi","weekday-2-long":"Mardi","weekday-3-long":"Mercredi","weekday-4-long":"Jeudi","weekday-5-long":"Vendredi","weekday-6-long":"Samedi","weekday-0-short":"dim","weekday-1-short":"lun","weekday-2-short":"mar","weekday-3-short":"mer","weekday-4-short":"jeu","weekday-5-short":"ven","weekday-6-short":"sam","month-0-long":"Janvier","month-1-long":"F\u00e9vrier","month-2-long":"Mars","month-3-long":"Avril","month-4-long":"Mai","month-5-long":"Juin","month-6-long":"Juillet","month-7-long":"Ao\u00fbt","month-8-long":"Septembre","month-9-long":"Octobre","month-10-long":"Novembre","month-11-long":"D\u00e9cembre","month-0-genitive":"janvier","month-1-genitive":"f\u00e9vrier","month-2-genitive":"mars","month-3-genitive":"avril","month-4-genitive":"mai","month-5-genitive":"juin","month-6-genitive":"juillet","month-7-genitive":"ao\u00fbt","month-8-genitive":"septembre","month-9-genitive":"octobre","month-10-genitive":"novembre","month-11-genitive":"d\u00e9cembre","month-0-short":"jan.","month-1-short":"f\u00e9v.","month-2-short":"mars","month-3-short":"avr.","month-4-short":"mai","month-5-short":"juin","month-6-short":"juil.","month-7-short":"ao\u00fbt","month-8-short":"sep.","month-9-short":"oct.","month-10-short":"nov.","month-11-short":"d\u00e9c.","minutes-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"\u00e0 l\u2019instant","one":["il y a ",{"type":"idOrVar","name":"value"}," minute"],"two":["il y a ",{"type":"idOrVar","name":"value"}," minutes"],"few":["il y a ",{"type":"idOrVar","name":"value"}," minutes"],"many":["il y a ",{"type":"idOrVar","name":"value"}," minutes"],"other":["il y a ",{"type":"idOrVar","name":"value"}," minutes"]}},"hours-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"\u00e0 l\u2019instant","one":["il y a ",{"type":"idOrVar","name":"value"}," heure"],"two":["il y a ",{"type":"idOrVar","name":"value"}," heures"],"few":["il y a ",{"type":"idOrVar","name":"value"}," heures"],"many":["il y a ",{"type":"idOrVar","name":"value"}," heures"],"other":["il y a ",{"type":"idOrVar","name":"value"}," heures"]}},"days-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"aujourd\u2019hui","one":"hier","two":["il y a ",{"type":"idOrVar","name":"value"}," jours"],"few":["il y a ",{"type":"idOrVar","name":"value"}," jours"],"many":["il y a ",{"type":"idOrVar","name":"value"}," jours"],"other":["il y a ",{"type":"idOrVar","name":"value"}," jours"]}},"weeks-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"cette semaine","one":"la semaine derni\u00e8re","two":["il y a ",{"type":"idOrVar","name":"value"}," semaines"],"few":["il y a ",{"type":"idOrVar","name":"value"}," semaines"],"many":["il y a ",{"type":"idOrVar","name":"value"}," semaines"],"other":["il y a ",{"type":"idOrVar","name":"value"}," semaines"]}},"months-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"ce mois-ci","one":"le mois dernier","two":["il y a ",{"type":"idOrVar","name":"value"}," mois"],"few":["il y a ",{"type":"idOrVar","name":"value"}," mois"],"many":["il y a ",{"type":"idOrVar","name":"value"}," mois"],"other":["il y a ",{"type":"idOrVar","name":"value"}," mois"]}},"years-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"cette ann\u00e9e","one":"l\u2019ann\u00e9e derni\u00e8re","two":["il y a ",{"type":"idOrVar","name":"value"}," ans"],"few":["il y a ",{"type":"idOrVar","name":"value"}," ans"],"many":["il y a ",{"type":"idOrVar","name":"value"}," ans"],"other":["il y a ",{"type":"idOrVar","name":"value"}," ans"]}},"minutes-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"\u00e0 l\u2019instant","one":["dans ",{"type":"idOrVar","name":"value"}," minute"],"two":["dans ",{"type":"idOrVar","name":"value"}," minutes"],"few":["dans ",{"type":"idOrVar","name":"value"}," minutes"],"many":["dans ",{"type":"idOrVar","name":"value"}," minutes"],"other":["dans ",{"type":"idOrVar","name":"value"}," minutes"]}},"hours-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"\u00e0 l\u2019instant","one":["dans ",{"type":"idOrVar","name":"value"}," heure"],"two":["dans ",{"type":"idOrVar","name":"value"}," heures"],"few":["dans ",{"type":"idOrVar","name":"value"}," heures"],"many":["dans ",{"type":"idOrVar","name":"value"}," heures"],"other":["dans ",{"type":"idOrVar","name":"value"}," heures"]}},"days-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"aujourd\u2019hui","one":"demain","two":["dans ",{"type":"idOrVar","name":"value"}," jours"],"few":["dans ",{"type":"idOrVar","name":"value"}," jours"],"many":["dans ",{"type":"idOrVar","name":"value"}," jours"],"other":["dans ",{"type":"idOrVar","name":"value"}," jours"]}},"weeks-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"cette semaine","one":"la semaine prochaine","two":["dans ",{"type":"idOrVar","name":"value"}," semaines"],"few":["dans ",{"type":"idOrVar","name":"value"}," semaines"],"many":["dans ",{"type":"idOrVar","name":"value"}," semaines"],"other":["dans ",{"type":"idOrVar","name":"value"}," semaines"]}},"months-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"ce mois-ci","one":"le mois prochain","two":["dans ",{"type":"idOrVar","name":"value"}," mois"],"few":["dans ",{"type":"idOrVar","name":"value"}," mois"],"many":["dans ",{"type":"idOrVar","name":"value"}," mois"],"other":["dans ",{"type":"idOrVar","name":"value"}," mois"]}},"years-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"cette ann\u00e9e","one":"l\u2019ann\u00e9e prochaine","two":["dans ",{"type":"idOrVar","name":"value"}," ans"],"few":["dans ",{"type":"idOrVar","name":"value"}," ans"],"many":["dans ",{"type":"idOrVar","name":"value"}," ans"],"other":["dans ",{"type":"idOrVar","name":"value"}," ans"]}},"minutes-ago-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"\u00e0 l\u2019instant","other":["il y a\u202f",{"type":"idOrVar","name":"value"},"\u202fmin"]}},"hours-ago-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"\u00e0 l\u2019instant","other":["il y a ",{"type":"idOrVar","name":"value"},"\u00a0h"]}},"days-ago-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"aujourd\u2019hui","other":["il y a ",{"type":"idOrVar","name":"value"},"\u00a0j"]}},"minutes-until-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"maintenant","other":["dans ",{"type":"idOrVar","name":"value"},"\u00a0min"]}},"hours-until-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"maintenant","other":["dans ",{"type":"idOrVar","name":"value"},"\u00a0h"]}},"days-until-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"aujourd\u2019hui","other":["dans ",{"type":"idOrVar","name":"value"},"\u00a0j"]}},"incorrectDate":"(date incorrecte)","dateTimeFormat_%c":"%a %e %b %Y %H:%M:%S","dateTimeFormat_%x":"%d/%m/%Y","dateTimeFormat_%X":"%H:%M:%S","shortTimeFormat":"%H:%M","shortTimeFormat12":"%I:%M %p","shortTimeFormat24":"%H:%M","shortDateTimeFormat":"%x %H:%M","weekStartsOnMonday":"1","firstDayOfTheWeek":"1","timePickerOrder":"HMp","datePickerOrder":"DMY","hourMinutesSeparator":":","listSeparator_middle":", ","time_am":"AM","time_pm":"PM","durationPattern":"hh:mm:ss.SS","brandShortName":"Vulpes","browserBrandShortName":"Vulpes","browserBrandFullName":"Project Vulpes \u2014 Firefox OS Community Project","ringtoneBrandName":"Par d\u00e9faut","ringtoneSectionBrandName":"syst\u00e8me","stopped_download_title":"T\u00e9l\u00e9chargement arr\u00eat\u00e9","stopped_download_message":"Le t\u00e9l\u00e9chargement a \u00e9t\u00e9 arr\u00eat\u00e9. Voulez-vous relancer le t\u00e9l\u00e9chargement\u202f?","stopped_download_left_button":"Annuler","stopped_download_right_button":"Relancer","failed_download_title":"Le t\u00e9l\u00e9chargement a \u00e9chou\u00e9","failed_download_message":{"value":["Le t\u00e9l\u00e9chargement de ",{"type":"idOrVar","name":"name"}," a \u00e9chou\u00e9. Voulez-vous relancer le t\u00e9l\u00e9chargement\u202f?"]},"failed_download_left_button":"Annuler","failed_download_right_button":"Relancer","stop_download_title":"Arr\u00eater le t\u00e9l\u00e9chargement","stop_download_message":"Voulez-vous vraiment arr\u00eater le t\u00e9l\u00e9chargement\u202f?","stop_download_left_button":"Non","stop_download_right_button":"Oui","delete_download_title":"Suppression","delete_download_message":{"value":["Supprimer ",{"type":"idOrVar","name":"name"},"\u202f?"]},"delete_download_left_button":"Annuler","delete_download_right_button":"Supprimer","delete_all_download_title":"Suppression","delete_all_download_left_button":"Annuler","delete_all_download_right_button":"Supprimer","delete_all_download_message":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"number"}]}],"value":{"zero":"Ne supprimer aucun fichier\u202f?","one":"Supprimer le fichier\u202f?","two":["Supprimer ",{"type":"idOrVar","name":"number"}," fichiers\u202f?"],"few":["Supprimer ",{"type":"idOrVar","name":"number"}," fichiers\u202f?"],"many":["Supprimer ",{"type":"idOrVar","name":"number"}," fichiers\u202f?"],"other":["Supprimer ",{"type":"idOrVar","name":"number"}," fichiers\u202f?"]}},"unsupported_file_type_download_title":"Ouverture impossible","unsupported_file_type_download_message":{"value":["Aucune application n\u2019est capable d\u2019ouvrir ",{"type":"idOrVar","name":"name"},". Voulez-vous conserver le fichier ou le supprimer\u202f?"]},"unsupported_file_type_download_left_button":"Conserver","unsupported_file_type_download_right_button":"Supprimer","file_not_found_download_title":"Fichier introuvable","file_not_found_download_message":"Le fichier est introuvable sur l\u2019appareil","file_not_found_download_right_button":"OK","unmounted_sdcard_2_download_title":"Carte m\u00e9moire utilis\u00e9e","unmounted_sdcard_2_download_message":"Veuillez d\u00e9brancher le t\u00e9l\u00e9phone","unmounted_sdcard_2_download_right_button":"OK","no_provider_download_title":"Partage impossible","no_provider_download_message":"Aucun service disponible","no_provider_download_right_button":"OK","no_memory_download_title":"Carte SD pleine","no_memory_download_message":"Il n\u2019y a plus assez de place sur votre carte SD pour enregistrer votre t\u00e9l\u00e9chargement. Veuillez supprimer quelques fichiers avant de relancer le t\u00e9l\u00e9chargement.","no_memory_download_right_button":"OK","no_sdcard_found_2_download_title":"Aucune carte m\u00e9moire","no_sdcard_found_2_download_message":"Ins\u00e9rez une carte m\u00e9moire ou s\u00e9lectionnez dans les param\u00e8tres un autre emplacement par d\u00e9faut pour les m\u00e9dias","no_sdcard_found_2_download_right_button":"OK","file_open_error_download_title":"Ouverture impossible","file_open_error_download_message":{"value":["Un probl\u00e8me est survenu lors de l\u2019ouverture de ",{"type":"idOrVar","name":"name"},". Voulez-vous conserver le fichier ou le supprimer\u202f?"]},"file_open_error_download_left_button":"Conserver","file_open_error_download_right_button":"Supprimer","restart_download_error":"Une erreur s\u2019est produite lors de la reprise du t\u00e9l\u00e9chargement. Veuillez relancer le t\u00e9l\u00e9chargement.","no-downloads":"Aucun t\u00e9l\u00e9chargement","byteUnit-B":"o","byteUnit-KB":"Ko","byteUnit-MB":"Mo","byteUnit-GB":"Go","byteUnit-TB":"To","byteUnit-PB":"Po","byteUnit-EB":"Eo","byteUnit-ZB":"Zo","byteUnit-YB":"Yo","fileSize":{"value":[{"type":"idOrVar","name":"size"},"\u00a0",{"type":"idOrVar","name":"unit"}]},"percentage":{"value":[{"type":"idOrVar","name":"percentage"},"\u00a0%"]},"partialResult":{"value":[{"type":"idOrVar","name":"partial"}," sur ",{"type":"idOrVar","name":"total"}]},"summary":{"value":[{"type":"idOrVar","name":"date"}," - ",{"type":"idOrVar","name":"status"}]},"download-stopped":"Arr\u00eat\u00e9","download-failed":"Le t\u00e9l\u00e9chargement a \u00e9chou\u00e9","open_downloaded_file":"Ouvrir","share_downloaded_file":"Partager","wallpaper_downloaded_file":"D\u00e9finir comme fond d\u2019\u00e9cran","ringtone_downloaded_file":"D\u00e9finir comme sonnerie","cancel_downloaded_file":"Annuler"}
\ No newline at end of file
+{"gaia-menu-cancel":"Annuler","internetSharing-usb-description":"Partager ma connexion Internet avec un appareil connecté par USB.","internetSharing-wifi-description":"Partager ma connexion Internet avec d’autres appareils connectés par Wi-Fi.","unplayable-ringtone":"Impossible de changer la sonnerie car le fichier sélectionné n’est pas lisible par ce téléphone.","findmydevice-signup-description":"Localiser mon appareil vous permet de localiser, verrouiller ou effacer votre téléphone","findmydevice-settings-description":"Localiser mon appareil vous permet de localiser, verrouiller ou effacer votre téléphone depuis un site web :","findmydevice-active-tracking":"Votre téléphone est localisé par un autre appareil depuis le site web.","findmydevice-not-tracking":"Aucun appareil ne localise actuellement votre téléphone depuis le site web.","ums-confirm":"Activer le stockage USB permet à un autre appareil ou ordinateur connecté par USB d’accéder aux fichiers de ce téléphone.","reset-device":"Réinitialiser le téléphone","reset-warning-body":"La réinitialisation effacera toutes vos données, y compris les applications que vous avez achetées. Le téléphone sera restauré aux valeurs d’usine. Aucun de vos paramètres ni aucune de vos données ne seront conservés.","reset-devtools-warning-body":"La réinitialisation effacera toutes vos données, y compris les applications que vous avez achetées. Aucun de vos paramètres ni aucune de vos données ne seront sauvegardés. Cependant votre téléphone possèdera des outils de développement sans restriction, vous permettant de modifier et déboguer les applications système.","unreset-devtools-warning-body":"La réinitialisation effacera toutes vos données, y compris les applications que vous avez achetées. Aucun de vos paramètres ni aucune de vos données ne seront sauvegardés. Votre téléphone ne possèdera plus les outils de développement sans restriction, vous ne pourrez plus modifier ni déboguer les applications système.","powerSave-explanation":"Le mode économie d’énergie désactive la connexion de données, le Bluetooth et la géolocalisation afin de préserver la charge de la batterie. Vous pourrez toujours activer à nouveau ces services manuellement.","performanceAndUsageDataInfo":{"value":["Aidez-nous à améliorer ",{"type":"idOrVar","name":"brandShortName"}," en partageant des informations concernant votre téléphone et la façon dont vous l’utilisez."]},"device-status-connected-device":"Connecté à l’audio du téléphone","device-status-connected-device-media":"Connecté à l’audio du téléphone/média","settings":"Paramètres","advancedSettings":"Paramètres avancés","default":"Défaut","enabled":"Activé","enable":"Activer","disabled":"Désactivé","error":"Erreur","ok":"OK","yes":"Oui","no":"Non","reset":"Réinitialiser","factory-reset":"Paramètres usine","delete":"Supprimer","cancel":"Annuler","close":"Fermer","back":"Retour","continue":"Continuer","skip":"Passer","pair":"Associer","add":"Ajouter","done":"Terminé","set":"Définir","notNow":"Plus tard","turnOn":"Activer","selectASIMCard":"Choisir une carte SIM","sim1":"SIM 1","sim2":"SIM 2","unavailable":"Indisponible","confirmation":"Confirmation","simSettingsWithIndex":{"value":["Paramètres SIM ",{"type":"idOrVar","name":"index"}]},"simWithIndex":{"value":["SIM ",{"type":"idOrVar","name":"index"}]},"networkAndConnectivity":"Réseau et connectivité","airplaneMode":{"value":"Mode avion","attrs":{"ariaLabel":"Mode avion"}},"geolocation":{"value":"Géolocalisation","attrs":{"ariaLabel":"Géolocalisation"}},"wifi":"Wi-Fi","wifi-header":"Wi-Fi","wifi-disabled":"Pour afficher les réseaux disponibles, activez le Wi-Fi.","availableNetworks":"Réseaux disponibles","knownNetworks":"Réseaux connus","hiddenNetworks":"Réseaux masqués","scanning":"Recherche…","scanNetworks":"Actualiser","full-status-initializing":"Initialisation…","full-status-connecting":{"value":["Connexion à ",{"type":"idOrVar","name":"ssid"},"…"]},"full-status-associated":"Obtention d’une adresse IP…","full-status-connected":{"value":["Connecté à ",{"type":"idOrVar","name":"ssid"}]},"full-status-connectingfailed":"Échec de connexion","full-status-disconnected":"Hors connexion","short-status-connecting":"Connexion…","short-status-associated":"Obtention d’une adresse IP…","short-status-connected":"Connecté","short-status-connectingfailed":"Échec de connexion","short-status-disconnected":"Hors connexion","wifi-sleep":"Veille Wi-Fi","wifi-sleep-description":"Éteint automatiquement le Wi-Fi lorsque l’appareil est en veille afin de préserver la charge de la batterie.","manageNetworks":"Gérer les réseaux","wifi-bad-credentials-title":"La connexion au Wi-Fi a échoué","wifi-bad-credentials-confirm":{"value":["Impossible de se connecter à ",{"type":"idOrVar","name":"ssid"},", l’authentification a échoué à cause d’identifiants incorrects. Veuillez saisir à nouveau le mot de passe du réseau."]},"manageNetworks-header":"Gérer les réseaux","manageCertificates":"Gérer les certificats","manageCertificates-header":"Gérer les certificats","joinHiddenNetwork":"Rejoindre un réseau masqué","securedBy":{"value":["Sécurisé par ",{"type":"idOrVar","name":"capabilities"}]},"security":"Sécurité","securityNone":"Aucune","securityOpen":"Ouvert","notInRange":"Hors de portée","ipAddress":"Adresse IP","linkSpeed":"Vitesse connexion","linkSpeedMbs":{"value":[{"type":"idOrVar","name":"linkSpeed"}," Mbit/s"]},"signalStrength":"Niveau de signal","signalLevel0":"Très faible","signalLevel1":"Faible","signalLevel2":"Moyen","signalLevel3":"Bon","signalLevel4":"Très bon","forget":"Oublier","forgetNetwork-dialog":"Oublier ce réseau ?","forgetNetwork-confirmation":"Confirmation","authentication":"Authentification","authentication-header":"Authentification","eap":"Méthode EAP","auth-phase2":"Authentification, étape 2","server-certificate":"Certificat du serveur","import-certificate-msg":"Attention : si vous devez importer de nouveaux certificats, veuillez vous rendre dans la section « Gestion des certificats ».","identity":"Identifiant","password":"Mot de passe","showPassword":"Afficher le mot de passe","networkNotification":"Notification de réseau","networkNotification-expl":"Me signaler quand un réseau ouvert est disponible","macAddress":"Adresse MAC","deviceInfo_software":{"value":[{"type":"idOrVar","name":"brandShortName"}," ",{"type":"idOrVar","name":"os"}]},"macUnavailable":"Non disponible","noNetworksFound":"Aucun réseau détecté","noKnownNetworks":"Aucun réseau connu","edit":"Modifier","certificateList":"Liste de certificats","noImportedCertificates":"Aucun certificat importé","deleteCertificate":"Supprimer le certificat","importCertificate":"Importer un certificat","selectCertificateFile":"Choisir fichier de certificat","enterCertificateNickname":"Surnom du certificat","nickname":"Surnom (nick name)","enter-certificate-name-msg":"Vous pouvez modifier le surnom (nick name) ou conserver le nom par défaut.","certificate-deletion-failed":"Échec de la suppression du certificat","certificate-deletion-failed-description":"Un problème a pu se produire avec le stockage, par exemple.","certificate-import-failed":"Échec de l’importation du certificat","certificate-import-failed-description":"Cela peut venir d’un fichier de certificat endommagé, par exemple.","wpsMessage":"Se connecter par WPS","wpsMessage-header":"Connexion WPS","fullStatus-wps-inprogress":"WPS en cours…","fullStatus-wps-canceled":"WPS a été annulé","fullStatus-wps-timedout":"WPS expiré","fullStatus-wps-failed-error":{"value":["WPS a échoué [",{"type":"idOrVar","name":"error"},"]"]},"fullStatus-wps-overlapped":"Un autre point d’accès WPS PBC a été détecté","wpsDescription2":"Configuration Wi-Fi automatique","wpsMethodSelection":"Sélectionner une méthode WPS :","wpsPbcLabel":"Avec le bouton","wpsMyPinLabel":"Avec mon code PIN","wpsApPinLabel":"Avec PIN point d’accès","wpsPinDescription":"Code PIN (4 à 8 chiffres)","wpsPinInput":{"value":["Saisir ",{"type":"idOrVar","name":"pin"}," sur l’autre appareil"]},"wpsPinAps":"Sélectionner l’autre appareil","wpsAnyAp":"N’importe lequel","wpsCancelMessage":"Annuler WPS","wpsCancelFailedMessageError":{"value":["Échec à l’annulation de WPS [",{"type":"idOrVar","name":"error"},"]"]},"callSettings":"Paramètres des appels","callSettings-header":"Paramètres des appels","callSettingsQuery":"Lecture des informations réseau…","callSettingsQueryError":"Erreur dans la requête","voiceMail":"Messagerie vocale","voiceMail-header":"Messagerie vocale","voiceMail-number":"Numéro de la messagerie","voiceMail-number-notSet":"Non défini","voicePrivacyMode":"Appels chiffrés","callWaiting":"Double appel","confirmCallWaitingTitle":"Impossible de confirmer les préférences du double appel","confirmCallWaitingDesc":"L’appareil n’est actuellement pas en mesure de communiquer avec l’opérateur. Essayez de définir la préférence à nouveau.","callForwarding":"Transfert d’appels","callForwarding-header":"Transfert d’appels","callForwardingForward":"Transférer","callForwardingUnconditional":"Toujours transférer","callForwardingUnconditionalShort":"Toujours","callForwardingMobileBusy":"Transférer si ligne occupée","callForwardingMobileBusyShort":"Si ligne occupée","callForwardingNoReply":"Transférer si sans réponse","callForwardingNoReplyShort":"Si sans réponse","callForwardingNotReachable":"Transférer si injoignable","callForwardingNotReachableShort":"Si injoignable","callForwardingForwardingVoiceToNumber":{"value":["Appels vocaux vers ",{"type":"idOrVar","name":"number"},""]},"callForwardingNotForwarding":"Désactivé","callForwardingConfirmTitle":"Message de confirmation du transfert d’appels","cf-alwaysUseThisNumber":"Toujours utiliser ce numéro","cf-numberWhenUnreachable":"Numéro si injoignable","cf-numberWhenUnanswered":"Numéro si sans réponse","cf-numberWhenBusy":"Numéro si ligne occupée","callForwardingInvalidNumberError":"Ce numéro de téléphone n’est pas valide.","callForwardingSetSuccess":"Opération définie avec succès.","callForwardingSetForbidden":"Votre opérateur ne prend pas en charge cette opération.","callForwardingSetError":"Erreur dans les paramètres d’appel.","callerId":"Présentation du numéro","callBarring":"Blocage d’appels","callBarring-header":"Blocage d’appels","callBarring-outgoing":"Appels sortants","callBarring-baoc":"Tous","callBarring-boic":"À l’international","callBarring-boicExhc":"À l’international, sauf pays de résidence","callBarring-incoming":"Appels entrants","callBarring-baic":"Tous","callBarring-baicR":"Tous (en itinérance)","callBarring-passcode":"Code secret","callBarring-change-passcode":"Changer le code secret","callBarring-passcode-enter":"Saisir le code secret","callBarring-passcode-unknown":"Si vous ne connaissez pas votre code secret, veuillez contacter votre opérateur","callBarring-current-passcode":"Code secret actuel","callBarring-new-passcode":"Nouveau code secret","callBarring-create-passcode":"Créer un code secret","callBarring-confirm-passcode":"Confirmer le code secret","callBarring-passcode-error-generic":"Erreur code secret.","callBarring-passcode-error-incorrect":"Le code secret est incorrect.","callBarring-passcode-error-mismatch":"Les codes secrets ne correspondent pas.","callBarring-update-item-error":{"value":["Erreur lors de l’actualisation de l’élément : ",{"type":"idOrVar","name":"error"},"."]},"callBarring-update-generic-error":"Erreur réseau ou code secret erroné","callBarring-change-passcode-success":"Le code secret a été changé.","callerId-default":"Valeur par défaut du réseau","callerId-hide":"Masquer le numéro","callerId-show":"Afficher le numéro","fdn":"Numérotation fixe","fdnSettings":"Numérotation fixe FDN","fdnSettings-header":"Numérotation fixe (FDN)","fdn-authorizedNumbers":"Numéros autorisés","fdn-authorizedNumbers-header":"Numéros autorisés","fdn-authorizedNumbers-desc":"Liste des numéros composables","fdn-explanation1":"Restreint les numéros qui peuvent être composés à partir de ce téléphone.","fdn-explanation2":"(Les numéros d’urgence seront toujours composables)","fdnEnable":"Activer FDN","fdnDisable":"Désactiver FDN","fdnReset":"Réinitialiser PIN2","fdnAttemptMsg3":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"one":"C’est votre dernier essai autorisé pour saisir le code correct avant de bloquer la carte SIM.","two":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant de bloquer la carte SIM."],"few":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant de bloquer la carte SIM."],"many":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant de bloquer la carte SIM."],"other":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant de bloquer la carte SIM."]}},"fdnLastChanceMsg":"C’est votre dernier essai autorisé pour saisir le code PIN2 correct. Sinon, vous devrez saisir le code PUK2 pour utiliser cette carte SIM.","fdnErrorMsg":"Le code PIN2 est incorrect.","fdnContact-name":"Nom","fdnContact-number":"Numéro","fdnContact-submit":"Valider","fdnAction-add":"Ajouter un contact","fdnAction-call":"Appeler","fdnAction-edit":"Modifier le contact","fdnAction-edit-header":"Modifier le contact","fdnAction-delete":"Supprimer le contact","fdnNoFDNFreeRecord":"Nombre maximum de numéros pour la numérotation fixe FDN atteint. Vous ne pouvez plus ajouter de numéros.","messagingSettings":"Paramètres des messages","messagingSettings-header":"Paramètres des messages","message-confirmation":"Confirmation des messages","message-delivery-reports":"Accusés de réception","message-delivery-reports-details":"Demander un accusé de réception pour chaque message envoyé","message-read-reports":"Confirmations de lecture","message-read-reports-details":"Demander une confirmation de lecture pour chaque message envoyé","mmsTitle":"Messages multimédias (MMS)","auto-retrieve":"Récupération automatique","auto-retrieve-details":"Récupérer automatiquement les messages","manual-retrieve":"Désactivé","auto-roaming-retrieve":"Toujours activé","auto-no-roaming-retrieve":"Activé sauf en itinérance","autoConfigure":"Configuration auto","wapPushMessages":"Messages Push WAP","wapPush":"Push WAP","cellBroadcastMessages":"Messages de diffusion cellulaire","cellBroadcast":"Diffusion cellulaire","emergencyAlert":"Alertes d’urgence","messagingInformation":"Informations","messaging-SMSC":"Centre de service SMS","messaging-sim-settings":{"value":["Paramètres SIM ",{"type":"idOrVar","name":"index"}]},"unknown-SMSC":"Information non disponible","cellularAndData":"Téléphonie et données","cellularAndData-header":"Téléphonie et données","dataOperator":"Réseau mobile","dataConnectivity":"Données","dataConnection":"Connexion de données","dataRoaming":"Itinérance de données","netOperator":"Réseaux mobiles","netOperator-header":"Réseaux mobiles","availableOp":"Réseaux mobiles à proximité","preferredNetworkTypeAlertTitle":"Type de réseau préféré","preferredNetworkTypeAlertErrorMessage":"Le type de réseau ne peut pas être enregistré","operator-networkType":"Type de réseau","operator-networkType-auto":"Automatique","operator-networkType-prefer3G":"3G de préférence","operator-networkType-2G":"2G uniquement","operator-networkType-3G":"3G uniquement","operator-networkType-GSM":"GSM","operator-networkType-WCDMA":"WCDMA","operator-networkType-CDMA":"CDMA","operator-networkType-EVDO":"EVDO","operator-networkType-LTE":"LTE","operator-networkType-preferWCDMA":"WCDMA de préférence","operator-networkType-auto-2G-3G":"Automatique (2G/3G)","operator-networkType-auto-3G-4G":"Automatique (3G/4G)","operator-networkType-auto-CDMA-EVDO":"Automatique (CDMA/EVDO)","operator-networkType-auto-WCDMA-GSM":"Automatique (WCDMA/GSM)","operator-networkType-auto-LTE-WCDMA-GSM":"Automatique (LTE/WCDMA/GSM)","operator-networkType-auto-LTE-WCDMA":"Automatique (LTE/WCDMA)","operator-networkType-auto-LTE-CDMA-EVDO":"Automatique (LTE/CDMA/EVDO)","operator-networkType-auto-WCDMA-GSM-CDMA-EVDO":"Automatique (WCDMA/GSM/CDMA/EVDO)","operator-autoSelect":"Sélection automatique","operator-networkSelect-auto":"Automatique","operator-networkSelect-manual":"Manuelle","operator-turnAutoSelectionOff":"Pour afficher les réseaux mobiles disponibles, désactivez la sélection automatique.","operator-info-connecting":"Connexion…","operator-info-failed":"Échec de connexion","operator-info-connected":"Connecté","operator-info-available":"Disponible","operator-info-current":"Connecté","operator-info-forbidden":"Interdit","operator-info-unknown":"État inconnu","operator-roamingPreference":"Préférence d’itinérance","operator-roamingPreference-home":"Réseau opérateur","operator-roamingPreference-any":"Tous les réseaux","operator-roamingPreference-affiliated":"Réseaux partenaires","apn":"Nom du point d’accès (APN)","apnSettings":"Paramètres APN","apnSettings-header":"Paramètres APN","dataSettings":"Paramètres données","dataSettings-header":"Paramètres données","suplSettings":"Paramètres A-GPS","suplSettings-header":"Paramètres A-GPS","messageSettings":"Paramètres messages","messageSettings-header":"Paramètres messages","dunSettings":"Paramètres mode modem","dunSettings-header":"Paramètres mode modem","imsSettings":"Paramètres IMS","imsSettings-header2":"Paramètres IMS","fotaSettings":"Paramètres FOTA","fotaSettings-header":"Paramètres FOTA","apn-editor-header":"Éditeur d’APN","mmsport":"Port MMS","mmsproxy":"Mandataire MMS","mmsc":"MMSC","httpProxyHost":"Hôte mandataire HTTP","httpProxyPort":"Port mandataire HTTP","custom":"(param. personnalisés)","apn-authType":"Authentification","apn-authType-notDefined":"Non définie","apn-authType-none":"Aucune","apn-authType-pap":"PAP","apn-authType-chap":"CHAP","apn-authType-papOrChap":"PAP ou CHAP","apn-protocol":"Protocole","apn-roaming-protocol":"Protocole en itinérance","apn-protocol-notDefined":"Non défini","apn-protocol-ip":"IPv4","apn-protocol-ipv6":"IPv6","apn-protocol-ipv4v6":"IPv4/IPv6","apn-type":"Type d’APN","apn-mtu":"MTU","dataConnection-warning-head":"Activer la connexion de données ?","dataConnection-warning-message":"Les applications récupèreront automatiquement des données par la connexion de données quand nécessaire. Des frais supplémentaires liés aux données peuvent s’appliquer.","dataConnection-warning-message-short":"Permettre de récupérer automatiquement des données par la connexion de données.","dataRoaming-warning-head":"Activer l’itinérance de données ?","dataRoaming-warning-message":"Selon votre contrat avec votre opérateur, des frais supplémentaires pour vos données peuvent s’appliquer si vous êtes en mode itinérance.","dataRoaming-warning-message-short":"Permettre de récupérer des données même en itinérance.","change-apn-warning-message1":"Selon votre contrat avec votre opérateur, modifier vos paramètres de Nom de point d’accès (APN) peut entraîner des frais supplémentaires en itinérance. Voulez-vous continuer ?","reset-apn-warning-title":"Réinitialiser les paramètres APN","reset-apn-warning-message":"Vos paramètres seront réinitialisés à leur valeur par défaut. Cette action est irréversible.","state-unknown":"État inconnu","state-available":"Disponible","state-connected":"Connecté","state-current":"Actuel","state-forbidden":"Interdit","delete-apn-confirm":"Supprimer cet APN ?","delete-apn":"Supprimer","add-apn":"Ajouter un nouvel APN","reset-apn":"Réinitialiser","apnSettings-reset":"Réinitialiser les paramètres APN","bluetooth":"Bluetooth","bluetooth-header":"Bluetooth","bluetooth-enable-msg":"Pour afficher les appareils à proximité, activez le Bluetooth.","bluetooth-paired-devices":"Appareils associés","bluetooth-devices-in-area":"Appareils à proximité","bluetooth-visible-to-all":"Visible par tous","bluetooth-name-maxlength-alert":{"value":["Le nom de votre appareil excède ",{"type":"idOrVar","name":"length"}," caractères. Veuillez réessayer."]},"bluetooth-address":"Adresse Bluetooth","bluetooth-address-unavailable":"Non disponible","bt-status-nopaired":"Aucun appareil associé","bt-status-turnoff":"Éteint","bt-status-paired":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"zero":[{"type":"idOrVar","name":"name"}],"one":[{"type":"idOrVar","name":"name"}," et un autre"],"two":[{"type":"idOrVar","name":"name"}," et ",{"type":"idOrVar","name":"n"}," autres"],"few":[{"type":"idOrVar","name":"name"}," et ",{"type":"idOrVar","name":"n"}," autres"],"many":[{"type":"idOrVar","name":"name"}," et ",{"type":"idOrVar","name":"n"}," autres"],"other":[{"type":"idOrVar","name":"name"}," et ",{"type":"idOrVar","name":"n"}," autres"]}},"device-status-tap-connect":"Appuyez pour établir la connexion","device-status-pairing":"Association avec l’appareil…","device-status-waiting":"En attente de l’autre appareil…","device-status-paired":"Associé","device-status-connecting":"Connexion…","device-status-connected":"Connecté","device-status-connected-media":"Connecté à l’audio du média","device-option-unpair":"Dissocier","device-option-confirm":"Confirmer","device-option-unpair-confirmation":"Confirmation","device-option-unpair-device":"Dissocier l’appareil ?","device-option-connect":"Connecter","device-option-disconnect":"Déconnecter","search-for-device":"Recherche d’appareils…","search-device":"Rechercher des appareils","rename-device":"Renommer mon appareil","unnamed-device":"Appareil inconnu","change-btdevice-name":"Changer le nom de l’appareil","change-btdevice-name-desc":"Ce nom apparaîtra sur vos autres appareils Bluetooth","bluetooth-new-name":"Nouveau nom","error-pair-title":"Impossible d’associer les appareils","error-pair-pincode":"Impossible de s’associer avec l’appareil. Vérifiez que le code PIN est correct.","error-pair-toofast":"Impossible de s’associer avec l’appareil. Requêtes trop nombreuses.","error-connect-title":"Impossible de connecter les appareils","error-connect-msg":"Vérifiez que l’appareil avec lequel vous essayez de vous connecter est toujours à portée et que le Bluetooth est activé.","unpair-title":"Dissocier un appareil connecté ?","unpair-msg":"Vous êtes en ce moment connecté avec cet appareil. Le dissocier vous déconnectera aussi.","unpair-confirm":{"value":[{"type":"idOrVar","name":"unpair-title"},"\\n",{"type":"idOrVar","name":"unpair-msg"}]},"nfc":{"value":"NFC","attrs":{"ariaLabel":"NFC"}},"internetSharing":"Partage de connexion","internetSharing-header":"Partage de connexion","internetSharing-usb":"USB","internetSharing-wifi":"Wi-Fi","wifi-hotspot":"Partage par Wi-Fi","wifi-hotspot-enabled-msg":{"value":["Point d’accès « ",{"type":"idOrVar","name":"hotspotSSID"}," » activé"]},"wifi-password":"Mot de passe","wifi-security":"Sécurité","usb-tethering":"Modem USB","hotspotSettings":"Paramètres du point d’accès","wifi-auth-username":"Nom d’utilisateur","wifi-auth-password":"Mot de passe","wifi-join-username":"Nom d’utilisateur","wifi-join-password":"Mot de passe","hotspotSettings-header":"Point d’accès","ssid-name":"Nom de réseau (SSID)","hotspot-security-open":"Ouvert","hotspot-security-wpa-psk":"WPA","hotspot-security-wpa2-psk":"WPA2","hotspot-password-desc":"Le mot de passe doit contenir au moins 8 caractères.","personalization":"Personnalisation","sound":{"value":"Son","attrs":{"ariaLabel":"Son"}},"sound-header":"Son","volume":"Volume","tones":"Sonneries","touch-tones":"Tonalité des touches","touch-tone-short":"Courte","touch-tone-long":"Longue","select-tone":"Choisir sonnerie","ringtones":"Sonneries","alerts":"Alertes","manage-tones":"Gérer les sonneries","other-sounds":"Autres sons","unlock-screen":"Déverrouillage écran","camera":"Appareil photo","sent-message":"Message envoyé","alarm-sound":"Alarmes","ringtone-and-notification-sound":"Sonneries et notifications","media-sound":"Médias","saving-tone":"Enregistrement…","no-ringtone-app":"Aucune application sonnerie trouvée pour gérer les sonneries.","display":{"value":"Affichage","attrs":{"ariaLabel":"Affichage"}},"display-header":"Affichage","brightness":"Luminosité","brightness-autoAdjust":"Ajustement automatique","lockScreen":"Écran de verrouillage","wallpaper":"Fond d’écran","screen-timeout":"Délai de l’écran de veille","one-minute":"1 minute","two-minutes":"2 minutes","five-minutes":"5 minutes","ten-minutes":"10 minutes","screen-timeout-never":"Jamais","lock-orientation":"Empêcher la rotation","navigation":{"value":"Navigation","attrs":{"ariaLabel":"Navigation"}},"navigation-header":"Navigation","edge-swipe-navigation":"Navigation latérale","edge-swipe-navigation-enable-msg":"Faites glisser votre doigt depuis le bord droit ou gauche de l’écran pour passer d’une application à l’autre.","notifications":{"value":"Notifications","attrs":{"ariaLabel":"Notifications"}},"notifications-header":"Notifications","lockscreen-notifications":"Sur l’écran de verrouillage","notifications-resend":"Au redémarrage","dateAndTime":"Date et heure","dateAndTime-header":"Date et heure","setTimeAutomatically":"Réglage automatique","timezoneMessage":"Fuseau horaire","dateMessage":"Date","timeMessage":"Heure","selectTimezoneContinent":"Choisir un continent","selectTimezone":"Choisir un fuseau horaire","time-format":"Format","time-format-time":"Format d’affichage de l’heure","useDefaultTimeFormat":"Par défaut","hour-12":"12 heures","hour-24":"24 heures","languageAndRegion":"Langue et région","language":"Langue","language-header":"Langue","region":"Région","more-languages":"Ajouter d’autres langues","sample-format":"Aperçu du format","longDateFormat":"%A %d %Eb %Y","shortDateFormat":"%d %Eb %Y","simManager":"Gestionnaire de SIM","simManager-header":"Gérer les cartes SIM","simSettings":"Paramètres SIM","sim-manager-outgoing-calls":"Appels sortants","sim-manager-outgoing-call-desc":"Les appels seront émis depuis","sim-manager-outgoing-messages":"Messages sortants","sim-manager-outgoing-messages-desc":"Les SMS seront envoyés depuis","sim-manager-outgoing-data":"Données","sim-manager-outgoing-data-description":"Service de données mobiles et paiements du Marketplace","sim-manager-always-ask":"Toujours demander","sim-pin-locked":"Code PIN verrouillé","no-operator":"Aucun opérateur","cant-disable-simcard-alert":"Au moins une carte SIM doit être active","disable-simcard-confirm":{"value":["La carte SIM ",{"type":"idOrVar","name":"disableCardIndex"}," ne peut recevoir aucun appel ni aucun message. Les appels et messages sortants configurés pour utiliser la carte SIM ",{"type":"idOrVar","name":"disableCardIndex"}," seront impossibles."]},"change-outgoing-data-confirm":"Le transfert de données en cours sera suspendu. Continuer quand même ?","home-screens":{"value":"Écrans d’accueil","attrs":{"ariaLabel":"Écrans d’accueil"}},"homescreen-layout":"Disposition","homescreen-icon-layout":"Disposition des icônes","homescreen-three-columns":"Trois colonnes","homescreen-four-columns":"Quatre colonnes","notice":"Avertissement","search":{"value":"Recherche","attrs":{"ariaLabel":"Recherche"}},"search-header":"Recherche","search-engine":"Moteur de recherche","search-suggestions":"Activer les suggestions","search-suggestions-description":"Les caractères que vous saisissez lorsque vous effectuez une recherche sont envoyés à plusieurs moteurs de recherche et seront utilisés pour vous retourner des suggestions de recherche. Des frais supplémentaires liés aux données peuvent s’appliquer lors de l’utilisation de cette fonctionnalité.","replaceable-home-screen":"Écran d’accueil remplaçable","change-home-screen":"Changer l’écran d’accueil","get-more-home-screens":"Obtenir plus d’écrans d’accueil","my-home-screens":"Mes écrans d’accueil","home-screen-description":"Description","delete-home-screen-button":"Supprimer l’écran d’accueil","keyboardSettings":"Paramètres du clavier","selectedKeyboards":"Claviers sélectionnés","keyboards":"Claviers","keyboards-header":"Claviers","numberKeyboardLayouts":"Dispositions de chiffres","optionKeyboardLayouts":"Dispositions d’options","keypad":"Clavier numérique","dialpad":"Clavier téléphonique","selectKeyboards":"Sélectionner des claviers","selectKeyboards-header":"Sélectionner des claviers","mustHaveOneKeyboard":{"value":["Au moins un clavier de type ",{"type":"idOrVar","name":"type"}," doit être sélectionné"]},"keyboardType-text":"texte","keyboardType-url":"url","keyboardType-number":"chiffres","defaultKeyboardEnabled":{"value":[{"type":"idOrVar","name":"appName"}," : ",{"type":"idOrVar","name":"layoutName"}," est défini comme méthode de saisie par défaut"]},"keyboard-layout-emoji":"Émoticônes","themes":{"value":"Thèmes","attrs":{"ariaLabel":"Thèmes"}},"themes-header":"Thèmes","fxa":"Comptes Firefox","accounts":"Gestion des comptes","fxa-accounts":"Comptes Firefox","fxa-accounts-header":"Comptes Firefox","fxa-overview":"Votre compte Firefox vous permet d’utiliser des fonctionnalités comme Localiser mon appareil et des applications comme le Firefox Marketplace.","fxa-invitation":"Créer un compte ou se connecter","fxa-confirm-account":"Confirmer le compte","fxa-verification-email-sent-msg":{"value":["Nous allons envoyer un message à : ",{"type":"idOrVar","name":"email"}]},"fxa-please-verify":"Veuillez cliquer sur le lien de vérification du courriel pour confirmer votre compte.","fxa-cancel-confirmation":"Annuler la confirmation du compte","fxa-resend-alert":{"value":["Un lien de vérification a été envoyé à ",{"type":"idOrVar","name":"email"},"."]},"fxa-dont-see-email-resend":"Vous n’avez rien reçu ? Renvoyer.","fxa-about":"À propos","fxa-logged-in-text":{"value":["Connecté en tant que ",{"type":"idOrVar","name":"email"}]},"fxa-options":"Options","fxa-sign-out":"Se déconnecter","fxa-confirm-email":{"value":["Veuillez confirmer ",{"type":"idOrVar","name":"email"}]},"findmydevice":"Localiser mon appareil","findmydevice-create-account":"Créer un compte ou se connecter","findmydevice-enable":"Localiser mon appareil","findmydevice-about":"À propos de Localiser mon appareil","findmydevice-enable-geolocation-prompt":"Activer la géolocalisation ?","findmydevice-enable-geolocation-description":"Pour afficher la position de votre appareil, vous devez activer la géolocalisation.","findmydevice-geolocation-enabled":"Géolocalisation activée","findmydevice-enable-network":"Cet appareil est actuellement hors connexion. Veuillez vous connecter à un réseau puis réessayer.","findmydevice-error":"Une erreur s’est produite dans Localiser mon appareil.","findmydevice-please-try":"Veuillez réessayer.","findmydevice-please-tap":"Veuilez appuyer pour réessayer.","fxsync-overview":"Connectez-vous pour synchroniser votre historique, vos mots de passe et bien plus","fxsync-get-started":"Commencer","fxsync-sync-user":"Compte Firefox","fxsync-disable":"Désactiver Firefox Sync","fxsync-last-synced":{"value":["Dernière synchronisation ",{"type":"idOrVar","name":"when"}]},"fxsync-collections":"Collections","fxsync-legal":"Mentions légales","privacyAndSecurity":"Vie privée et sécurité","passcode-lock":"Verrouillage par code","passcode-lock-desc":{"value":["Code d’accès : ",{"type":"idOrVar","name":"code"}]},"screenLock":"Verrouillage","screenLock-header":"Verrouillage","screenLock-enabled-with-no-passcode":"Activé sans code d’accès","screenLock-enabled-with-passcode":"Activé avec code d’accès","simSecurity":"Sécurité carte SIM","simSecurity-header":"Sécurité carte SIM","appPermissions":{"value":"Permissions applications","attrs":{"ariaLabel":"Permissions applications"}},"appPermissions-header":"Permissions applications","author":"Auteur","permissions":"Permissions","revoke":"Révoquer","ask":"Demander","block":"Bloquer","allow2":"Autoriser","uninstallApp":"Désinstaller l’application","phone":"Téléphone","messages":"Messages","vibrate":"Vibreur","ring":"Sonnerie","change":"Changer","create":"Créer","create-a-passcode":"Créer un code d’accès","passcode":"Code d’accès","passcode-heading":"Code d’accès","enter-passcode":"Saisir le code d’accès","new-passcode":"Nouveau code","current-passcode":"Code d’accès actuel","incorrect-passcode":"Le code d’accès est incorrect.","passcode-doesnt-match":"Les codes d’accès ne correspondent pas. Veuillez réessayer.","confirm-passcode":"Confirmer le code d’accès","immediately":"Immédiatement","after-one-minute":"Au bout d’une minute","after-five-minutes":"Au bout de 5 minutes","after-fifteen-minutes":"Au bout de 15 minutes","after-thirty-minutes":"Au bout de 30 minutes","after-one-hour":"Au bout d’une heure","require-passcode":"Demander le code d’accès","change-passcode":"Changer le code d’accès","noSimCard":"Aucune carte SIM","simPin":"Code PIN carte SIM","simPin2":"Code PIN2 carte SIM","simPinWithIndex":{"value":["Code PIN carte SIM ",{"type":"idOrVar","name":"index"}]},"pukCode":"Code PUK","puk2Code":"Code PUK2","whatIsSimPin":"Qu’est-ce qu’un code PIN ?","simPinIntro1":"Le code PIN de la carte SIM empêche l’accès aux données réseau de votre carte SIM. Quand il est activé, tout appareil équipé de cette carte SIM réclamera le code PIN au démarrage.","simPinIntro2":"Un code PIN de carte SIM est différent du code destiné à déverrouiller l’appareil.","changeSimPin":"Modifier le code PIN","simPinChangedSuccessfully":"Code PIN carte SIM correctement modifié","simPinChangedSuccessfullyWithIndex":{"value":["Code PIN carte SIM ",{"type":"idOrVar","name":"index"}," correctement modifié"]},"pinTitle":"Saisir le code PIN","pin2Title":"Saisir le code PIN2","pukTitle":"Saisir le code PUK","puk2Title":"Saisir le code PUK2","newpinTitle":"Nouveau code PIN","pinErrorMsg":"Le code PIN est incorrect.","inputCodeRetriesLeft":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"one":"Dernier essai.","two":[{"type":"idOrVar","name":"n"}," essais restants."],"few":[{"type":"idOrVar","name":"n"}," essais restants."],"many":[{"type":"idOrVar","name":"n"}," essais restants."],"other":[{"type":"idOrVar","name":"n"}," essais restants."]}},"pinAttemptMsg3":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"one":"C’est votre dernier essai autorisé pour saisir le code correct avant de bloquer la carte SIM.","two":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant de bloquer la carte SIM."],"few":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant de bloquer la carte SIM."],"many":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant de bloquer la carte SIM."],"other":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant de bloquer la carte SIM."]}},"pinLastChanceMsg":"C’est votre dernier essai autorisé pour saisir le code PIN correct. Sinon, vous devrez saisir le code PUK pour utiliser cette carte SIM.","simCardLockedMsg":"La carte SIM est verrouillée.","simCardBlockedMsg":"La carte SIM est bloquée.","enterPukMsg":"Vous devez saisir la Personal Unlocking Key (PUK, soit Clé personnelle de déblocage) pour la carte SIM. Veuillez vous référer à la documentation associée à votre carte SIM ou contacter votre opérateur pour en savoir plus.","enterPuk2Msg":"Vous devez saisir la Personal Unlocking Key (PUK2, soit Clé personnelle de déblocage) pour la carte SIM. Veuillez vous référer à la documentation associée à votre carte SIM ou contacter votre opérateur pour en savoir plus.","pukErrorMsg":"Le code PUK est incorrect.","pukAttemptMsg3":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"n"}]}],"value":{"one":"C’est votre dernier essai autorisé pour saisir le code correct avant que cette carte SIM ne devienne définitivement inutilisable. Veuillez vous référer à la documentation associée à votre carte SIM ou contacter votre opérateur pour en savoir plus.","two":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant que cette carte SIM ne devienne définitivement inutilisable. Veuillez vous référer à la documentation associée à votre carte SIM ou contacter votre opérateur pour en savoir plus."],"few":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant que cette carte SIM ne devienne définitivement inutilisable. Veuillez vous référer à la documentation associée à votre carte SIM ou contacter votre opérateur pour en savoir plus."],"many":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant que cette carte SIM ne devienne définitivement inutilisable. Veuillez vous référer à la documentation associée à votre carte SIM ou contacter votre opérateur pour en savoir plus."],"other":["Il vous reste ",{"type":"idOrVar","name":"n"}," essais pour saisir le code correct avant que cette carte SIM ne devienne définitivement inutilisable. Veuillez vous référer à la documentation associée à votre carte SIM ou contacter votre opérateur pour en savoir plus."]}},"pukLastChanceMsg":"C’est votre dernier essai autorisé pour saisir le code PUK correct. Votre carte SIM deviendra définitivement inutilisable si vous saisissez un code PUK incorrect. Veuillez vous référer à la documentation associée à votre carte SIM ou contacter votre opérateur pour en savoir plus.","newSimPinMsg":"Nouveau code PIN (4 à 8 chiffres)","newSimPin2Msg":"Nouveau code PIN2 (4 à 8 chiffres)","confirmNewSimPinMsg":"Confirmer le nouveau code PIN","confirmNewSimPin2Msg":"Confirmer le nouveau code PIN2","newPinErrorMsg":"Les codes PIN ne correspondent pas","simLockedPhone":"Téléphone verrouillé sur ce réseau","unknownSimCardState":"État carte SIM inconnu","simCardNotReady":"Carte SIM pas encore prête","simCardIllegal":"Carte SIM non autorisée","genericLockError":"Une erreur inconnue s’est produite","doNotTrack":"Ne pas me pister","doNotTrack-header":"Ne pas me pister","doNotTrackActions":"Indiquer aux sites et applications que je ne souhaite pas être pisté","allowTracking":"Indiquer aux sites et applications que je souhaite être pisté","doNotHavePrefOnTracking":"Ne rien indiquer aux sites et applications concernant mes préférences de pistage","doNotTrack-dt":"Principe de fonctionnement","doNotTrack-dd1":"Lorsque vous activez Ne pas me pister, votre appareil informe tous les sites web et applications (aussi bien les publicitaires que les autres fournisseurs de contenu) que vous ne souhaitez pas que votre comportement soit pisté.","doNotTrack-dd2":"Activer cette fonctionnalité n’affectera en rien votre capacité à vous identifier sur des sites web, ni ne fera oublier à votre appareil vos informations personnelles, telles que le contenu de vos paniers d’achats, vos informations de géolocalisation, ou vos informations d’identification.","browsingPrivacy":{"value":"Confidentialité navigation","attrs":{"ariaLabel":"Confidentialité navigation"}},"browsingPrivacy-header":"Confidentialité navigation","confirm-clear-history-title":"Effacer l’historique","confirm-clear-history-desc":"Voulez-vous vraiment effacer votre historique ? Dans le cas où vous utilisez votre compte Firefox, l’historique synchronisé sera également effacé.","confirm-clear-cookies-cache-title":"Effacer les cookies et le cache","confirm-clear-cookies-cache-desc":"Voulez-vous vraiment effacer les cookies et le cache ?","clear":"Effacer","privateBrowsing":"Navigation privée","privateBrowsingDetails":"Ouvrir tous les liens externes dans une nouvelle fenêtre privée.","trackingProtection":"Protection contre le pistage","trackingProtectionDetails":"Protéger votre navigation contre les éléments de pistage potentiels dans toutes les nouvelles fenêtres.","browser-privacy-history-header":"Historique","browser-privacy-history-desc":"Effacer l’historique de navigation de cet appareil. Dans le cas où vous utilisez votre compte Firefox, l’historique synchronisé sera également effacé.","browser-privacy-history-button":"Effacer l’historique","browser-privacy-cookies-cache-header":"Cookies et cache","browser-privacy-cookies-cache-desc":"Effacer les données stockées (comme les cookies) de cet appareil. Les informations d’identification et les préférences seront perdues.","browser-privacy-cookies-cache-button":"Effacer les cookies et le cache","storage":"Stockage","enableUSBStorage1":"Stockage USB","umsUnplugToDisable":"Débrancher pour désactiver","appStorage":"Stockage des applications","appStorage-header":"Stockage des applications","storageSize":{"value":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}]},"availableSize":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"size"}]}],"value":{"zero":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," disponible"],"one":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," disponible"],"two":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," disponibles"],"few":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," disponibles"],"many":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," disponibles"],"other":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}," disponibles"]}},"apps-total-space":"Espace total","apps-used-space":"Utilisé","apps-free-space":"Libre","storage-details":"Les photos, vidéos et la musique sont stockées à un emplacement différent de celui des applications. Voir Stockage des médias pour plus de détails.","ums-shared-volumes":{"value":["Partage de : ",{"type":"idOrVar","name":"list"}]},"transferProtocol-header":"Protocole de transfert","protocol-mtp":"Protocole de transfert média (MTP)","protocol-ums":"Stockage USB de masse (UMS)","mediaStorage":"Stockage des médias","mediaStorage-header":"Stockage des médias","storage-name-internal":"Stockage interne","storage-name-external-0":"Stockage carte SD","storage-name-external-1":"Stockage carte SD 2","short-storage-name-internal":"Interne","short-storage-name-external-0":"Carte SD","short-storage-name-external-1":"Carte SD 2","music-space":"Musique","pictures-space":"Photos","videos-space":"Vidéos","left-space":"Espace libre","free-space":"Espace libre","total-space":"Espace total","size-not-available":"Indisponible","no-storage":"Aucun support de stockage détecté","advanced":"Avancé","ums-warning-title":"Activer le stockage USB de masse ?","default-media-location":"Emplacement des médias","default-media-location-msg":"Choisir où sont stockés par défaut les photos, vidéos, la musique et les téléchargements.","change-default-media-location-confirmation":"Changer le volume de stockage par défaut des médias affectera l’emplacement où seront enregistrés par défaut les médias. Les données existantes resteront à leur emplacement actuel. Voulez-vous continuer ?","eject-sdcard-external-0":"Éjecter la carte SD","eject-sdcard-external-1":"Éjecter la carte SD 2","format-sdcard-internal":"Formater le stockage interne","format-sdcard-external-0":"Formater la carte SD","format-sdcard-external-1":"Formater la carte SD 2","formatting":"Formatage…","unmount-sdcard-confirmation":"Vous ne pourrez plus accéder au contenu stocké sur la carte SD après l’avoir éjectée. Voulez-vous continuer ?","format-sdcard-title":"Formater la carte SD","format-sdcard-message":"Toutes les données stockées sur votre carte SD seront effacées, comme par exemple les photos ou la musique.","format-sdcard-internal-title":"Formater le stockage interne","format-sdcard-internal-message":"Toutes les données stockées sur le stockage interne seront effacées, comme par exemple les photos ou la musique.","format-sdcard-btnformat":"Formater","sdcardUnmounted":"Carte SD éjectée","ums-partial-support-warning-title":"Attention : le stockage USB de masse peut ne pas fonctionner","ums-partial-support-warning-msg-1":"Le stockage USB de masse ne fonctionne qu’avec une carte SD externe. Si vous souhaitez accéder à la fois à votre espace de stockage interne et externe, vous devez utiliser MTP.","ums-partial-support-warning-msg-2":"Choisissez « OK » pour activer le stockage USB de masse, ou « Annuler » pour rester sur le mode MTP.","device":"Appareil","help":{"value":"Aide","attrs":{"ariaLabel":"Aide"}},"deviceInfo":{"value":"Informations","attrs":{"ariaLabel":"Informations"}},"deviceInfo-header":"Informations","deviceInfo-IMEI":"IMEI","deviceInfo-IMEI-with-index":{"value":["IMEI ",{"type":"idOrVar","name":"index"}," : ",{"type":"idOrVar","name":"imei"},""]},"deviceInfo-ICCID":"ICCID","deviceInfo-ICCID-with-index":{"value":["SIM ",{"type":"idOrVar","name":"index"}," : ",{"type":"idOrVar","name":"iccid"},""]},"deviceInfo-MSISDN":"Numéro de téléphone","deviceInfo-MSISDN-with-index":{"value":["SIM ",{"type":"idOrVar","name":"index"}," : ",{"type":"idOrVar","name":"msisdn"},""]},"deviceInfo-ICCID-unavailable-sim":{"value":["SIM ",{"type":"idOrVar","name":"index"}," : indisponible"]},"downloads":{"value":"Téléchargements","attrs":{"ariaLabel":"Téléchargements"}},"downloads-header":"Téléchargements","unknown-phoneNumber":"Inconnu","unknown-phoneNumber-sim":{"value":["SIM ",{"type":"idOrVar","name":"index"}," : inconnu"]},"gitInfo":"Informations Git","model-name":"Modèle","os-version":"Version du système","firmware_revision":"Révision du micrologiciel","hardware_manufacturer":"Constructeur du matériel","hardware_platform":"Plate-forme du matériel","hardware_revision":"Révision du matériel","platform_version":"Version de la plate-forme","build-id":"Identifiant de version","update-channel":"Canal de mise à jour","build_number":"Numéro de compilation","update-url":"Adresse de mise à jour","last-updated":"Dernière mise à jour","more-info":"Plus d’informations","more-info-header":"Plus d’informations","softwareUpdates":"Mises à jour logicielles","check-update-daily":"Chaque jour","check-update-weekly":"Chaque semaine","check-update-monthly":"Chaque mois","check-for-updates":"Rechercher des mises à jour","check-update-now":"Rechercher maintenant","update-status":"État des mises à jour","no-network-when-update":"Veuillez vous connecter à Internet pour poursuivre la mise à jour.","checking-for-update":"Recherche de mises à jour…","no-updates":"Aucune mise à jour disponible","retry-when-online":"Le réseau est déconnecté. La recherche s’effectuera une fois connecté au réseau.","already-latest-version":{"value":["Vous possédez déjà la dernière version de ",{"type":"idOrVar","name":"brandShortName"}]},"check-error":"Une erreur s’est produite lors de la recherche de mises à jour.","aboutBrowserOS":{"value":["À propos de ",{"type":"idOrVar","name":"brandShortName"}]},"browser-os-desc":{"value":[{"type":"idOrVar","name":"brandShortName"}," est le système d’exploitation libre et ouvert de Mozilla. Notre mission est de promouvoir l’ouverture et l’innovation, et de faire en sorte que la puissance du Web reste entre vos mains."]},"learn-more":"En savoir plus","reset-head":"Réinitialisation","reset-warning-title":"ATTENTION","addons-auto-update":"Mise à jour automatique des modules","addons-update-notify":"M’en informer après l’installation","your-rights":"Vos droits","your-rights-header":"Vos droits","about-your-rights-0":{"value":[{"type":"idOrVar","name":"brandShortName"}," repose sur un logiciel libre et ouvert réalisé par une communauté de milliers de contributeurs dans le monde. Voici ce que vous devriez savoir :"]},"about-your-rights-1":{"value":[{"type":"idOrVar","name":"brandShortName"}," est fourni selon les termes de plusieurs licences libres dont la Mozilla Public License. Un appareil ",{"type":"idOrVar","name":"brandShortName"}," peut également contenir des logiciels propriétaires de tierces parties. Tout code fourni sous une licence libre vous donne le droit de le modifier et de distribuer vos versions modifiées à condition de respecter les termes de sa licence."]},"about-your-rights-2":{"value":["Il ne vous est cédé aucun droit ou licence sur les marques déposées de la Fondation Mozilla ou de tiers, y compris et sans limitation, sur le nom et le logo de ",{"type":"idOrVar","name":"brandShortName"},"."]},"about-your-rights-3":{"value":["Certaines fonctionnalités de ",{"type":"idOrVar","name":"brandShortName"},", comme le rapporteur de plantages, vous permettent de fournir un retour à Mozilla. En choisissant d’envoyer ces informations, vous donnez à Mozilla la permission d’utiliser ce retour pour améliorer ses logiciels, de le publier sur ses sites web et de le distribuer."]},"about-legal-info":"Informations légales","about-credit-info":"Contributeurs","about-legal-info-header":"Informations légales","open-source-notices":"Mentions légales open source","open-source-notices-header":"Mentions légales","obtaining-source-code":"Obtenir le code source","obtaining-source-code-header":"Obtenir le code source","about-mozilla":"Mozilla","about-other":"Autre","your-privacy":"Votre vie privée","your-privacy-header":"Votre vie privée","licensing-1":"Les copies exécutables de ce logiciel sont fournies par le projet Mozilla sous la Mozilla Public License 2.0 (MPL). Découvrez vos droits.","licensing-2":"La totalité du code source de ce logiciel est disponible sous des licences libres et open source. La plus grande partie en est disponible sous la licence Mozilla Public License 2.0 (MPL).","licensing-3":"Les autres parties du logiciel qui ne sont pas disponibles selon la licence Mozilla Public License 2.0 MPL sont disponibles selon différentes autres licences. Celles qui imposent la reproduction du texte de licence sont reprises ci-dessous. (Note : votre copie de ce logiciel peut ne pas contenir de code couvert par certaines des licences listées ici, cela dépend de la version précise du logiciel que vous avez choisie.)","licensing-MPL":"Mozilla Public License 2.0","licensing-Apache":"Apache License 2.0","developer":{"value":"Développeurs","attrs":{"ariaLabel":"Développeurs"}},"developer-header":"Développeurs","hud-settings-button":"Informations développeurs","hud-settings-header":"Informations développeurs","hud-devtools":"Outils de développement","hud-devtools-header":"Outils de développement","hud-hide":"Cacher tous les éléments HUD","hud-logging":"Changements dans ADB","hud-system":"Informations système","hud-problems":"Problèmes","hud-warnings":"Avertissements","hud-errors":"Erreurs","hud-security":"Problèmes de sécurité","hud-performance":"Performances","hud-reflows":"Reflows","hud-jank":"Blocages","hud-jank-threshold":"Seuil de blocage (ms)","hud-reflows-threshold":"Durée du seuil (ms)","hud-fps":"Images par seconde","hud-ttl":"Durée de chargement","hud-memory":"Mémoire","hud-uss":"Unique Set Size","hud-appmemory":"Mémoire application","hud-jsobjects":"Objets JS","hud-jsstrings":"Chaînes JS","hud-jsother":"JS (autre)","hud-dom":"DOM","hud-style":"Style","hud-other":"Autres","software":"Logiciel","devSettings":"Paramètres pour les développeurs","debug":"Débogage","developer-menu":"Menu développeurs","developer-tools":"Outils de développement","graphics":"Accélération graphique","hardware-composer":"Composition matérielle","paint-flashing":"Zones repeintes","dumps-layers":"Informations arbres de calques","dumps-display-list":"Informations display list","dumps-layerscope":"Informations layerscope","apz-overscroll":"Overscrolling","tiling":"Tuilage","low-precision":"Painting basse précision","low-opacity":"Transparence basse précision","draw-tile-borders":"Dessiner bords du tuilage","draw-layer-borders":"Dessiner bords du calque","log-animations":"Enregistrer les animations lentes","remote-debugging-usb":"Débogage USB","devtools-wifi":"Outils de développement par Wi-Fi","devtools-device-name":"Nom pour les outils de dév. Wi-Fi","devtools-pseudolocalization":"Pseudo-langues","debugger-disabled":"Désactivé","debugger-adb-only":"ADB uniquement","debugger-adb-and-devtools":"ADB et outils de développement","wifi-debugging":"Sortie Wi-Fi dans ADB","bluetooth-debugging":"Sortie Bluetooth dans ADB","bluetooth-snoop-debugging":"Espionnage Bluetooth HCI sur la carte SD","nfc-debugging":"Sortie NFC dans ADB","ril-debugging":"Sortie RIL dans ADB","network-debugging":"Sortie réseau dans ADB","mms-debugging":"Sortie MMS dans ADB","console-enabled":"Activer la console","cards-view-screenshots":"Miniatures : captures d’écran","geolocation-debugging-enabled":"Sortie géolocalisation dans ADB","geolocation-debugging-gps-locations-ignored":"Ignorer les positions GPS","use-reviewer-certs":"Certificats relecteur Marketplace","shake-to-log":"Secouer pour enregistrer les journaux système","enable-logshake-qa":"Activer le mode QA LogShake","launch-ftu":"Tutoriel de prise en main","no-ftu":"Aucun tutoriel de prise en main trouvé","software-home-button":"Émuler le bouton Accueil","home-gesture":"Geste de retour à l’accueil","continuous-transition":"Transition continue","debug-gaia-enabled":"Traces de débogage de Gaia","app-transition":"Transition d’application","app-suspending":"Suspension d’application","copypaste":"Copier-coller","multi-screen":"Multi-écrans","verbose-app-permissions":"Permissions d’applications détaillées","pinning-the-web":"Mode Épingler le Web","view-source":"Activer le geste pour afficher le code source","homescreen-paging":"Activer la pagination de l’écran d’accueil","window-management":"Gestion de la fenêtre","unlock-privileges":"Déverrouillage des privilèges","full-devtools":"Outils de développement non restreints","reset-devtools-warning-title":"ATTENTION","service-worker":"Service workers","service-worker-scope":"Portée","service-worker-script-spec":"Script spec","service-worker-current-worker-url":"Adresse URL du worker courant","service-worker-active-cache-name":"Nom du cache actuel","service-worker-waiting-cache-name":"Nom du cache en attente","service-worker-update":"Mise à jour","service-worker-unregister":"Désenregistrement","service-worker-no-registrations":"Aucun service worker trouvé","developer-menu-disabled-warning-title":"Menu développeur non actif","developer-menu-disabled-warning-msg-1":"Le mode développeur ne peut être activé si le menu développeur n’est pas d’abord activé.","developer-menu-disabled-warning-msg-2":"Merci de l’activer depuis Paramètres > Informations > Plus d’informations > Menu développeur.","enable-full-dev-mode-first-warning-title":"Voulez-vous activer le mode développeur ?","enable-full-dev-mode-first-warning-msg-1":"CETTE ACTION DÉTRUIRA TOUTES VOS DONNÉES ET EFFECTUERA UNE REMISE À ZÉRO COMPLÈTE.","enable-full-dev-mode-first-warning-msg-2":"CETTE ACTION DÉSACTIVERA TOUTES LES SÉCURITÉS SUR VOTRE APPAREIL.","enable-full-dev-mode-final-warning-title":"Voulez-vous activer le mode développeur ?","enable-full-dev-mode-final-warning-msg":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"count"}]}],"value":{"one":["Appuyez sur « Activer » ",{"type":"idOrVar","name":"count"}," fois de plus pour continuer."],"two":["Appuyez sur « Activer » ",{"type":"idOrVar","name":"count"}," fois de plus pour continuer."],"few":["Appuyez sur « Activer » ",{"type":"idOrVar","name":"count"}," fois de plus pour continuer."],"many":["Appuyez sur « Activer » ",{"type":"idOrVar","name":"count"}," fois de plus pour continuer."],"other":["Appuyez sur « Activer » ",{"type":"idOrVar","name":"count"}," fois de plus pour continuer."]}},"downloads-edit":"Mode édition","downloads-select-all":"Tout sélectionner","downloads-deselect-all":"Tout désélectionner","downloads-delete":"Supprimer","battery":"Batterie","battery-header":"Batterie","battery-level-percent":{"value":[{"type":"idOrVar","name":"level"}," %"]},"battery-current-level":{"value":["Niveau actuel (",{"type":"idOrVar","name":"level"}," %)"]},"battery-charging-calculating":"Calcul de la durée de charge…","battery-discharging-calculating":"Calcul du temps restant…","battery-charging-complete":"Charge complète","battery-discharging-complete":"Batterie épuisée","battery-time-hour":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"hours"}]}],"value":{"zero":[{"type":"idOrVar","name":"hours"}," h"],"one":[{"type":"idOrVar","name":"hours"}," h"],"two":[{"type":"idOrVar","name":"hours"}," h"],"few":[{"type":"idOrVar","name":"hours"}," h"],"many":[{"type":"idOrVar","name":"hours"}," h"],"other":[{"type":"idOrVar","name":"hours"}," h"]}},"battery-time-minute":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"minutes"}]}],"value":{"zero":[{"type":"idOrVar","name":"minutes"}," min"],"one":[{"type":"idOrVar","name":"minutes"}," min"],"two":[{"type":"idOrVar","name":"minutes"}," min"],"few":[{"type":"idOrVar","name":"minutes"}," min"],"many":[{"type":"idOrVar","name":"minutes"}," min"],"other":[{"type":"idOrVar","name":"minutes"}," min"]}},"battery-charging-minutes":{"value":[{"type":"idOrVar","name":"battery-time-minute"}," avant charge complète"]},"battery-charging-hours":{"value":[{"type":"idOrVar","name":"battery-time-hour"}," ",{"type":"idOrVar","name":"battery-time-minute"}," avant charge complète"]},"battery-discharging-minutes":{"value":["Batterie épuisée dans ",{"type":"idOrVar","name":"battery-time-minute"}]},"battery-discharging-hours":{"value":["Batterie épuisée dans ",{"type":"idOrVar","name":"battery-time-hour"}," ",{"type":"idOrVar","name":"battery-time-minute"}]},"powerSaveModeTitle":"Mode économie d’énergie","powerSaveMode":"Économie d’énergie","powerSave-threshold":{"value":["À ",{"type":"idOrVar","name":"level"}," % restants"]},"powerSave-never":"Jamais","turnOnAuto":"Activer automatiquement","accessibility":{"value":"Accessibilité","attrs":{"ariaLabel":"Accessibilité"}},"accessibility-header":"Accessibilité","screenReader":"Lecteur d’écran","screenReader-header":"Lecteur d’écran","screenReader-toggle":"Lecteur d’écran","colorFilter":"Filtres de couleurs","colorFilter-header":"Filtres de couleurs","colorFilter-toggle":"Filtres de couleurs","accessibility-audio":"Audio","accessibility-audio-monoAudio":"Audio mono","mono-enabled":"Mono activé","mono-disabled":"Mono désactivé","screenReader-captions":"Afficher des sous-titres","screenReader-enable-msg":"Lorsque le lecteur d’écran est activé, votre appareil fournira un retour vocal pour guider les utilisateurs aveugles et malvoyants.","screen-reader-speech-volume":"Volume de lecture","screen-reader-speech-rate":"Vitesse de lecture","screenReader-confirm-title-start":"Activer le lecteur d’écran ?","screenReader-confirm-description-start":"Activer le lecteur d’écran modifiera le fonctionnement de l’écran tactile. Appuyez à n’importe quel endroit pour entendre l’élément situé sous votre doigt, et appuyez deux fois pour l’activer.","screenReader-confirm-button-start":"Activer","screenReader-confirm-title-stop":"Désactiver le lecteur d’écran ?","screenReader-confirm-description-stop":"Désactiver le lecteur d’écran arrêtera tout retour vocal.","screenReader-confirm-button-stop":"Désactiver","colorFilter-invert":"Inverser","colorFilter-grayscale":"Nuances de gris","colorFilter-contrast":"Contraste","accessibility-input":"Modes de saisie","accessibility-input-desc":"Ajustements pour les gestes et le tactile","tap-hold-delay":"Délai de l’appui long","short-hold-delay":"Court","medium-hold-delay":"Moyen","long-hold-delay":"Long","improveBrowserOS":{"value":["Améliorer ",{"type":"idOrVar","name":"brandShortName"}],"attrs":{"ariaLabel":{"value":["Améliorer ",{"type":"idOrVar","name":"brandShortName"}]}}},"improveBrowserOSTitle":{"value":["Améliorer ",{"type":"idOrVar","name":"brandShortName"}]},"sendMozillaFeedback":"Donner votre avis à Mozilla","performanceAndUsageData":"Données performances et utilisation","sharePerformanceAndUsageData":"Envoyer les données","appUsageByRegion":"Utilisation des applications","crashReports":"Rapports de plantage","crashReports-header":"Rapports de plantage","crashReportInfo":"Envoyer un rapport à Mozilla lorsqu’un plantage se produit nous permet de résoudre le problème pour tout le monde. Les rapports sont envoyés uniquement par Wi-Fi.","alwaysSendReport":"Toujours envoyer","neverSendReport":"Ne jamais envoyer","askToSendReport2":"Toujours demander","crash-reports-description-1":"Un rapport de plantage contient des détails concernant le plantage et votre appareil, ainsi qu’un instantané de l’état de votre appareil lors du plantage.","crash-reports-description-2":"Il peut inclure différentes choses comme les applications et pages ouvertes, le texte saisi dans les formulaires et le contenu des messages ouverts, l’historique de navigation récent, ou la géolocalisation utilisée par une application ouverte.","crash-reports-description-3-start":"Nous utilisons les rapports de plantage pour essayer de résoudre les problèmes et améliorer nos logiciels. Nous gérons vos informations comme décrit dans notre ","crash-reports-description-3-privacy":"politique de confidentialité","crash-reports-description-3-end":".","sendFeedbackTitle":"Donner votre avis","feedback-description":"Votre avis nous aidera à améliorer Firefox OS.","feedback-happy":"Satisfait","feedback-sad":"Déçu","feedback-mood-description":"Quel est votre sentiment ?","feedback_whyfeel_happy":"Vous êtes satisfait ?","feedback_whyfeel_sad":"Vous êtes déçu ?","feedback-mood-explain":"Pouvez-vous nous expliquer en détail ?","feedback-public-description":"Votre avis sera rendu public, assurez-vous donc de ne pas inclure d’informations personnelles.","feedback-contactyou":"Cochez cette case pour nous permettre de vous contacter par courriel.","email-address1":"Adresse électronique :","email-keepprivate":"Pas d’inquiétude, votre adresse électronique restera confidentielle.","feedback-sendbtn":"Envoyer mon avis","feedback-errormessage-connect-error":"Le système ne peut pas envoyer votre avis car la connexion a échoué. Veuillez corriger le problème de connexion et l’envoyer à nouveau.","feedback-errormessage-server-off":"Le système ne peut pas envoyer votre avis lors de la phase d’arrêt du serveur. Veuillez patienter un instant et l’envoyer à nouveau.","feedback-errormessage-timeout":"Le système ne peut pas envoyer votre avis car le délai d’attente de la connexion est dépassé. Veuillez redémarrer et l’envoyer à nouveau.","feedback-errormessage-wrong-email":"Le système ne peut pas envoyer votre avis car l’adresse électronique n’est pas renseignée ou son format est incorrect. Veuillez renseigner une adresse électronique correcte et l’envoyer à nouveau.","feedback-errormessage-empty-comment":"Le système ne peut pas envoyer votre avis car le champ commentaire est vide. Veuillez renseigner le formulaire et l’envoyer à nouveau.","feedback-complete-msg":"Merci d’avoir donné votre avis ! Nous l’utiliserons pour améliorer les prochaines versions de Firefox OS.","feedback-errormessage-unknown-error":"Une erreur est survenue. Merci de réessayer.","online-support":"Aide en ligne :","call-support":"Aide par téléphone :","user-guide":"Manuel de l’utilisateur","default-support":"Pour obtenir de l’aide, veuillez contacter votre revendeur.","simToolkit-header":"Boîte à outils SIM","operatorServices":"Services de l’opérateur","operatorServices-help":"Aide","operatorServices-helpmenu":"Menu d’aide","stkAppsNotAvailable":"Applications de la carte SIM indisponibles.","stkItemsNaiSetUpCall":"Configurer appels","stkItemsNaiSendSs":"Envoyer SS","stkItemsNaiSendUssd":"Envoyer USSD","stkItemsNaiSendSms":"Envoyer SMS","stkItemsNaiSendDtmf":"Envoyer DTMF","stkItemsNaiLaunchBrowser":"Lancer le navigateur","stkItemsNaiPlayTone":"Écouter sonneries","stkItemsNaiDisplayText":"Afficher texte","stkItemsNaiGetInkey":"Effectuer une saisie","stkItemsNaiGetInput":"Effectuer une saisie","stkItemsNaiSelectItem":"Sélectionner élément","stkItemsNaiSetUpMenu":"Configurer menu","stkItemsNaiProvideLocalInfo":"Fournir des informations locales","stkItemsNaiSetIdleModeText":"Définir texte quand inactif","stkItemsNaiPerformCardApdu":"Lancer l’APDU de la carte","stkItemsNaiPowerOnCard":"Activer la carte","stkItemsNaiPowerOffCard":"Désactiver la carte","stkItemsNaiGetReaderStatus":"Obtenir l’état du lecteur","stkItemsNaiOpenChannel":"Ouvrir canal","stkItemsNaiCloseChannel":"Fermer canal","stkItemsNaiReceiveData":"Recevoir données","stkItemsNaiSendData":"Envoyer données","stkItemsNaiGetChannelStatus":"Obtenir l’état du canal","stkItemsNaiEndOfTheProactiveSession":"Fin de la session proactive","stkEntry":{"attrs":{"ariaLabel":{"value":[{"type":"idOrVar","name":"title"}]}}},"is-warning-storage-header":"Stockage USB","is-warning-tethering-header":"Modem USB","is-warning-wifi-header":"Point d’accès Wi-Fi","is-warning-storage-tethering-message":"Activer le stockage USB désactivera le mode modem USB. Voulez-vous activer le stockage USB ?","is-warning-tethering-storage-message":"Activer le mode modem USB désactivera le stockage USB. Voulez-vous activer le mode modem USB ?","is-warning-tethering-wifi-message":"Activer le mode modem USB désactivera le point d’accès Wi-Fi. Voulez-vous activer le mode modem USB ?","is-warning-wifi-tethering-message":"Activer le point d’accès Wi-Fi désactivera le mode modem USB. Voulez-vous activer le point d’accès Wi-Fi ?","settings-dialog-alert":"Alerte","settings-dialog-confirm":"Confirmation","settings-dialog-prompt":"Invite","settings-alert-dialog-header":{"value":[{"type":"idOrVar","name":"settings-dialog-alert"}]},"settings-confirm-dialog-header":{"value":[{"type":"idOrVar","name":"settings-dialog-confirm"}]},"settings-prompt-dialog-header":{"value":[{"type":"idOrVar","name":"settings-dialog-prompt"}]},"accessibility-app-icon":{"attrs":{"ariaLabel":{"value":["Application ",{"type":"idOrVar","name":"appName"}]}}},"addon":"Module","addons":{"value":"Modules complémentaires","attrs":{"ariaLabel":"Modules complémentaires"}},"addons-header":"Modules complémentaires","addon-details-header1":{"value":[{"type":"idOrVar","name":"appName"}]},"addon-enable":"Activer le module","addon-description-header":"Description","addon-developer-name":{"value":[{"type":"idOrVar","name":"developerName"}]},"addon-targets":"Applications modifiées","addon-options":"Options","addon-delete":"Supprimer le module","addon-share":"Partager le module","addon-share-no-provider":"Pas de partage d’application possible","addon-no-description":"Ce module ne contient pas de description dans son fichier manifeste.","addon-no-targets":"Ce module ne modifie aucune application installée.","addon-restart-apps-to-disable":"Module désactivé ; redémarrer les applications modifiées.","addon-reboot-required":"Ce module modifie des applications système ; vous devez redémarrer votre appareil pour le désactiver complètement.","addon-rename":"Renommer","addon-rename-input":"Nom du module","addon-rename-desc":"Changer le nom de votre module","addon-obsolete":"Ce module est obsolète et ne fonctionne pas.","addon-obsolete-can-delete":"Ce module est obsolète et ne fonctionne pas. Veuillez le supprimer.","my-addons":"Mes modules","get-more-addons":"Installer d’autres modules","achievements":{"value":"Badges","attrs":{"ariaLabel":"Badges"}},"achievements-customize-champ-name":"Champion de la personnalisation","achievements-customize-champ-description":"Ouvrez le Customizer dans une application","achievements-add-on-all-star-name":"Créateur aguerri","achievements-add-on-all-star-description":"Utilisez le Customizer pour créer votre propre module","achievements-fab-finder-name":"Explorateur curieux","achievements-fab-finder-description":"Installez une application ou un module depuis le Hackerplace","achievements-content-commander-name":"Commandant du contenu","achievements-content-commander-description":"Envoyez une application ou un module sur le Hackerplace","achievements-sharing-is-caring-name":"Le partage c’est la santé","achievements-sharing-is-caring-description":"Partagez vos applications et modules avec l’application Partage","achievements-creative-kind-name":"Esprit créatif","achievements-creative-kind-description":"Utilisez l’éditeur de thèmes pour créer un nouveau thème","service-workers-interception":"Intercepter les requêtes","service-workers-testing":"Activer sur HTTP","closeButton":{"attrs":{"ariaLabel":"Fermer"}},"changeWallpaperButton":{"attrs":{"ariaLabel":"Changer le fond d’écran"}},"editButton":{"attrs":{"ariaLabel":"Modifier"}},"wifiSection":{"attrs":{"ariaLabel":"Wi-Fi"}},"simManagerSection":{"attrs":{"ariaLabel":"Gestionnaire de SIM"}},"callSettingsSection":{"attrs":{"ariaLabel":"Paramètres des appels"}},"messagingSettingsSection":{"attrs":{"ariaLabel":"Paramètres des messages"}},"cellularAndDataSection":{"attrs":{"ariaLabel":"Téléphonie et données"}},"bluetoothSection":{"attrs":{"ariaLabel":"Bluetooth"}},"internetSharingSection":{"attrs":{"ariaLabel":"Partage de connexion"}},"dateAndTimeSection":{"attrs":{"ariaLabel":"Date et heure"}},"languageSection":{"attrs":{"ariaLabel":"Langue"}},"keyboardsSection":{"attrs":{"ariaLabel":"Claviers"}},"fxa-accountsSection":{"attrs":{"ariaLabel":"Comptes Firefox"}},"findmydeviceSection":{"attrs":{"ariaLabel":"Localiser mon appareil"}},"fxsyncSection":{"attrs":{"ariaLabel":"Firefox Sync"}},"screenLockSection":{"attrs":{"ariaLabel":"Verrouillage"}},"simSecuritySection":{"attrs":{"ariaLabel":"Sécurité carte SIM"}},"doNotTrackSection":{"attrs":{"ariaLabel":"Ne pas me pister"}},"enableStorageSection":{"attrs":{"ariaLabel":"Stockage USB"}},"mediaStorageSection":{"attrs":{"ariaLabel":"Stockage des médias"}},"appStorageSection":{"attrs":{"ariaLabel":"Stockage des applications"}},"batterySection":{"attrs":{"ariaLabel":"Batterie"}},"helpImprove":"Aidez-nous à améliorer nos logiciels et services en envoyant automatiquement des données statistiques. Des frais supplémentaires liés aux données peuvent s’appliquer.","learn-more-privacy":"Lisez notre politique de confidentialité","basicMetricsRadio":"Envoi basique","enhancedMetricsRadio":"Envoi amélioré","noneMetricsRadio":"Aucun envoi","basicMetricsExplain":"Envoyer l’utilisation des applications et les rapports d’erreurs.","enhancedMetricsExplain":"Envoyer les rapports basiques plus des informations de diagnostic.","noneMetricsExplain":"Ne pas envoyer de données.","fxsync":"Firefox Sync","fxsync-collections-bookmarks":"Marque-pages","fxsync-collections-history":"Historique","fxsync-collections-passwords":"Mots de passe","fxsync-empty-account":"Ce compte est vide. Vous devez d’abord lancer la synchronisation à partir de Firefox, Firefox pour Android ou Firefox pour iOS puis réessayer ici.","fxsync-error-invalid-account":"Ce compte est inactif.","fxsync-error-invalid-account-explanation":"Vous devez d’abord activer Firefox Sync sur un ordinateur ou un appareil Android puis réessayer.","fxsync-error-no-key-fetch-token":"Veuillez réessayer de vous connecter.","fxsync-error-no-key-fetch-token-explanation":"Nous avons détecté une erreur au sein de la session actuelle de votre compte Firefox.","fxsync-error-offline":"Aucune connexion réseau.","fxsync-error-offline-explanation":"Vous devez être en ligne pour utiliser Firefox Sync.","fxsync-error-unknown":"Oups, une erreur s’est produite.","fxsync-error-unknown-explanation":"Nous nous excusons pour la gêne occasionnée. Veuillez réessayer plus tard.","fxsync-tos":"Conditions d’utilisation","fxsync-privacy":"Politique de confidentialité","fxsync-signing":"Connexion…","fxsync-sync-now":"Synchroniser maintenant","fxsync-syncing":"Synchronisation en cours","ringtone-none":"Aucune","ringer_bubble":"Dragon","ringer_chimer":"Partage","ringer_cunning":"Bleu","ringer_dulco":"Ambiance asiatique","ringer_firefox2":{"value":[{"type":"idOrVar","name":"ringtoneBrandName"}]},"ringer_garden":"Croissance","ringer_marbled":"Ramune","ringer_opti":"Transformation","ringer_pecko":"Pivert","ringer_pingy":"Téléportation","ringer_poppop":"Alerte vaisseau spatial","ringer_sata":"Lien","ringer_simple":"Anthropique","ringer_standard1":"Hyperbole","ringer_standard2":"Malice","ringer_stutter":"Phoniatrie","ringer_symmetry":"Autrui","ringer_tailwind":"Afrique","ringer_thoughtful":"Rêve","ringer_wit":"Raison","ringer_zen":"Verrillon","notifier_bells":"Campaniforme","notifier_bloom":"Enfance","notifier_chime":"Tatouage","notifier_clicka":"Fontaine de bambou","notifier_digi":"Arc","notifier_dropplet":"Goutte d’eau","notifier_firefox2":{"value":[{"type":"idOrVar","name":"ringtoneBrandName"}]},"notifier_hello":"Nouveau message","notifier_lowmarble":"Écho","notifier_ping":"Sonar","notifier_plickle":"Idiophone","notifier_pocko":"Abeille","notifier_quado":"Lipogramme","notifier_red":"Évolution","notifier_sato":"Aux frontières du réel","notifier_swipo":"Luxure","notifier_trickleup":"Résonance","notifier_trink":"Saccharification","notifier_verse":"Bille","tz-region":"Région","tz-city":"Ville","tzRegion-Africa":"Afrique","tzRegion-America":"Amérique","tzRegion-Antarctica":"Antarctique","tzRegion-Asia":"Asie","tzRegion-Atlantic":"Océan Atlantique","tzRegion-Australia":"Australie","tzRegion-Europe":"Europe","tzRegion-Indian":"Océan Indien","tzRegion-Pacific":"Océan Pacifique","title-app":"Permission d’application","title-web":"Permission de site web","title-cam":"Choix de la caméra","perm-alarms":"Programmer des alarmes","perm-audio-capture":"Microphone","perm-audio-capture-appRequest":{"value":["Voulez-vous partager votre microphone avec l’application ",{"type":"idOrVar","name":"app"}," ?"]},"perm-audio-capture-webRequest":{"value":["Voulez-vous partager votre microphone avec ",{"type":"idOrVar","name":"site"}," ?"]},"perm-audio-channel-content":"Lire des sons","perm-audio-channel-notification":"Lire notifications sonores","perm-audio-channel-ringer":"Lire des sonneries","perm-background-sensors":"Données du capteur","perm-bluetooth":"Bluetooth","perm-browser":"Inspecter des pages web","perm-camera":"Caméras","perm-camera-selector-appRequest":{"value":["Veuillez sélectionner une caméra pour l’application ",{"type":"idOrVar","name":"app"},"."]},"perm-camera-selector-webRequest":{"value":["Veuillez sélectionner une caméra pour ",{"type":"idOrVar","name":"site"},"."]},"perm-camera-appRequest":{"value":["L’application ",{"type":"idOrVar","name":"app"}," souhaite utiliser votre caméra."]},"perm-camera-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite utiliser votre caméra."]},"perm-cellbroadcast":"Diffusions cellulaires","perm-contacts":"Contacts","perm-contacts-appRequest":{"value":["L’application ",{"type":"idOrVar","name":"app"}," souhaite accéder à votre liste de contacts."]},"perm-contacts-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite accéder à votre liste de contacts."]},"perm-desktop-notification":"Notifications","perm-desktop-notification-appRequest":{"value":["L’application ",{"type":"idOrVar","name":"app"}," souhaite afficher des notifications."]},"perm-desktop-notification-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite afficher des notifications."]},"perm-device-storage-apps":"Stockage applications","perm-device-storage-music":"Stockage musique","perm-device-storage-music-appRequest":{"value":["L’application ",{"type":"idOrVar","name":"app"}," souhaite accéder à votre musique."]},"perm-device-storage-music-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite accéder à votre musique."]},"perm-device-storage-pictures":"Stockage images","perm-device-storage-pictures-appRequest":{"value":["L’application ",{"type":"idOrVar","name":"app"}," souhaite accéder à vos images."]},"perm-device-storage-pictures-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite accéder à vos images."]},"perm-device-storage-sdcard":"Stockage carte mémoire","perm-device-storage-sdcard-appRequest":{"value":["L’application ",{"type":"idOrVar","name":"app"}," souhaite accéder à votre carte mémoire."]},"perm-device-storage-sdcard-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite accéder à votre carte mémoire."]},"perm-device-storage-videos":"Stockage vidéos","perm-device-storage-videos-appRequest":{"value":["L’application ",{"type":"idOrVar","name":"app"}," souhaite accéder à vos vidéos."]},"perm-device-storage-videos-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite accéder à vos vidéos."]},"perm-embed-apps":"Embarquer des applications","perm-fmradio":"Radio FM","perm-fmradio-appRequest":{"value":["L’application ",{"type":"idOrVar","name":"app"}," souhaite utiliser la radio FM."]},"perm-fmradio-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite utiliser la radio FM."]},"perm-geolocation":"Géolocalisation","perm-geolocation-appRequest":{"value":["L’application ",{"type":"idOrVar","name":"app"}," souhaite connaître votre position."]},"perm-geolocation-more-info":{"value":[{"type":"idOrVar","name":"brandShortName"}," utilise les données du GPS, du Wi-Fi et des antennes de téléphonie mobile environnantes pour déterminer approximativement votre position. Les données sont envoyées à Mozilla ainsi qu’à d’autres fournisseurs de services et sont mises en commun afin d’améliorer la précision des positions. Les données peuvent être stockées sur votre appareil et des données peuvent être collectées même lorsqu’aucune application n’est exécutée."]},"perm-geolocation-webRequest":{"value":[{"type":"idOrVar","name":"site"}," souhaite connaître votre position."]},"perm-idle":"Consulter l’état de l’inactivité","perm-indexedDB-chrome-settings":"IndexedDB en mode privilégié","perm-indexedDB-unlimited":"Stockage IndexedDB illimité","perm-input-manage":"Gérer les claviers","perm-input":"Comportement d’un clavier","perm-media-capture":"Caméra et microphone","perm-media-capture-appRequest":{"value":["Voulez-vous partager votre caméra et votre microphone avec l’application ",{"type":"idOrVar","name":"app"}," ?"]},"perm-media-capture-webRequest":{"value":["Voulez-vous partager votre caméra et votre microphone avec ",{"type":"idOrVar","name":"site"}," ?"]},"perm-mobileconnection":"Connexion mobile","perm-mobileid":"Identité mobile","perm-network-events":"Évènements réseau","perm-open-remote-window":"Lancer une application","perm-permissions":"Changer des permissions","perm-power":"Paramètres d’énergie","perm-settings":"Changer des paramètres","perm-systemXHR":"Requêtes cross-origin","perm-telephony":"Téléphonie","perm-video-capture":"Caméra","perm-video-capture-appRequest":{"value":["Voulez-vous partager votre caméra avec l’application ",{"type":"idOrVar","name":"app"}," ?"]},"perm-video-capture-webRequest":{"value":["Voulez-vous partager votre caméra avec ",{"type":"idOrVar","name":"site"}," ?"]},"perm-voicemail":"Contrôler messagerie vocale","perm-webapps-manage":"Installer applications web","perm-wifi-manage":"Gérer le Wi-Fi","perm-attention":"Ouvrir des pop-up","perm-audio-channel-alarm":"Lire les sonneries d’alarme","perm-audio-channel-normal":"Lire des sons","perm-audio-channel-publicnotification":"Lire notifications publiques","perm-audio-channel-telephony":"Lire sonneries téléphoniques","perm-backgroundservice":"Service d’arrière-plan","perm-device-storage-crashes":"Stocker données de plantage","perm-downloads":"Accéder aux téléchargements","perm-embed-widgets":"Embarquer des widgets","perm-feature-detection":"Détecter les fonctionnalités","perm-firefox-accounts":"Comptes Firefox","perm-mobilenetwork":"Réseau mobile","perm-moz-attention":"Ouvrir des pop-up (moz)","perm-moz-audio-channel-ringer":"Lire des sonneries (moz)","perm-moz-audio-channel-telephony":"Lire sonneries téléph. (moz)","perm-moz-firefox-accounts":"Comptes Firefox (moz)","perm-networkstats-manage":"Statistiques consommation","perm-nfc":"NFC","perm-nfc-hci-events":"Évènements NFC HCI","perm-nfc-manager":"Gérer le NFC","perm-phonenumberservice":"Service numéro de téléphone","perm-push":"API Push","perm-resourcestats-manage":"Statistiques de ressources","perm-sms":"Lire et envoyer des SMS","perm-speaker-control":"Contrôler le volume","perm-storage":"Accéder au stockage","perm-tcp-socket":"Sockets TCP","perm-test-permission":"Permission test","perm-time":"Régler la date et l’heure","perm-udp-socket":"Sockets UDP","perm-wappush":"Push WAP","weekday-0-long":"Dimanche","weekday-1-long":"Lundi","weekday-2-long":"Mardi","weekday-3-long":"Mercredi","weekday-4-long":"Jeudi","weekday-5-long":"Vendredi","weekday-6-long":"Samedi","weekday-0-short":"dim","weekday-1-short":"lun","weekday-2-short":"mar","weekday-3-short":"mer","weekday-4-short":"jeu","weekday-5-short":"ven","weekday-6-short":"sam","month-0-long":"Janvier","month-1-long":"Février","month-2-long":"Mars","month-3-long":"Avril","month-4-long":"Mai","month-5-long":"Juin","month-6-long":"Juillet","month-7-long":"Août","month-8-long":"Septembre","month-9-long":"Octobre","month-10-long":"Novembre","month-11-long":"Décembre","month-0-genitive":"janvier","month-1-genitive":"février","month-2-genitive":"mars","month-3-genitive":"avril","month-4-genitive":"mai","month-5-genitive":"juin","month-6-genitive":"juillet","month-7-genitive":"août","month-8-genitive":"septembre","month-9-genitive":"octobre","month-10-genitive":"novembre","month-11-genitive":"décembre","month-0-short":"jan.","month-1-short":"fév.","month-2-short":"mars","month-3-short":"avr.","month-4-short":"mai","month-5-short":"juin","month-6-short":"juil.","month-7-short":"août","month-8-short":"sep.","month-9-short":"oct.","month-10-short":"nov.","month-11-short":"déc.","minutes-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"à l’instant","one":["il y a ",{"type":"idOrVar","name":"value"}," minute"],"two":["il y a ",{"type":"idOrVar","name":"value"}," minutes"],"few":["il y a ",{"type":"idOrVar","name":"value"}," minutes"],"many":["il y a ",{"type":"idOrVar","name":"value"}," minutes"],"other":["il y a ",{"type":"idOrVar","name":"value"}," minutes"]}},"hours-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"à l’instant","one":["il y a ",{"type":"idOrVar","name":"value"}," heure"],"two":["il y a ",{"type":"idOrVar","name":"value"}," heures"],"few":["il y a ",{"type":"idOrVar","name":"value"}," heures"],"many":["il y a ",{"type":"idOrVar","name":"value"}," heures"],"other":["il y a ",{"type":"idOrVar","name":"value"}," heures"]}},"days-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"aujourd’hui","one":"hier","two":["il y a ",{"type":"idOrVar","name":"value"}," jours"],"few":["il y a ",{"type":"idOrVar","name":"value"}," jours"],"many":["il y a ",{"type":"idOrVar","name":"value"}," jours"],"other":["il y a ",{"type":"idOrVar","name":"value"}," jours"]}},"weeks-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"cette semaine","one":"la semaine dernière","two":["il y a ",{"type":"idOrVar","name":"value"}," semaines"],"few":["il y a ",{"type":"idOrVar","name":"value"}," semaines"],"many":["il y a ",{"type":"idOrVar","name":"value"}," semaines"],"other":["il y a ",{"type":"idOrVar","name":"value"}," semaines"]}},"months-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"ce mois-ci","one":"le mois dernier","two":["il y a ",{"type":"idOrVar","name":"value"}," mois"],"few":["il y a ",{"type":"idOrVar","name":"value"}," mois"],"many":["il y a ",{"type":"idOrVar","name":"value"}," mois"],"other":["il y a ",{"type":"idOrVar","name":"value"}," mois"]}},"years-ago-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"cette année","one":"l’année dernière","two":["il y a ",{"type":"idOrVar","name":"value"}," ans"],"few":["il y a ",{"type":"idOrVar","name":"value"}," ans"],"many":["il y a ",{"type":"idOrVar","name":"value"}," ans"],"other":["il y a ",{"type":"idOrVar","name":"value"}," ans"]}},"minutes-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"à l’instant","one":["dans ",{"type":"idOrVar","name":"value"}," minute"],"two":["dans ",{"type":"idOrVar","name":"value"}," minutes"],"few":["dans ",{"type":"idOrVar","name":"value"}," minutes"],"many":["dans ",{"type":"idOrVar","name":"value"}," minutes"],"other":["dans ",{"type":"idOrVar","name":"value"}," minutes"]}},"hours-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"à l’instant","one":["dans ",{"type":"idOrVar","name":"value"}," heure"],"two":["dans ",{"type":"idOrVar","name":"value"}," heures"],"few":["dans ",{"type":"idOrVar","name":"value"}," heures"],"many":["dans ",{"type":"idOrVar","name":"value"}," heures"],"other":["dans ",{"type":"idOrVar","name":"value"}," heures"]}},"days-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"aujourd’hui","one":"demain","two":["dans ",{"type":"idOrVar","name":"value"}," jours"],"few":["dans ",{"type":"idOrVar","name":"value"}," jours"],"many":["dans ",{"type":"idOrVar","name":"value"}," jours"],"other":["dans ",{"type":"idOrVar","name":"value"}," jours"]}},"weeks-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"cette semaine","one":"la semaine prochaine","two":["dans ",{"type":"idOrVar","name":"value"}," semaines"],"few":["dans ",{"type":"idOrVar","name":"value"}," semaines"],"many":["dans ",{"type":"idOrVar","name":"value"}," semaines"],"other":["dans ",{"type":"idOrVar","name":"value"}," semaines"]}},"months-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"ce mois-ci","one":"le mois prochain","two":["dans ",{"type":"idOrVar","name":"value"}," mois"],"few":["dans ",{"type":"idOrVar","name":"value"}," mois"],"many":["dans ",{"type":"idOrVar","name":"value"}," mois"],"other":["dans ",{"type":"idOrVar","name":"value"}," mois"]}},"years-until-long":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"cette année","one":"l’année prochaine","two":["dans ",{"type":"idOrVar","name":"value"}," ans"],"few":["dans ",{"type":"idOrVar","name":"value"}," ans"],"many":["dans ",{"type":"idOrVar","name":"value"}," ans"],"other":["dans ",{"type":"idOrVar","name":"value"}," ans"]}},"minutes-ago-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"à l’instant","other":["il y a ",{"type":"idOrVar","name":"value"}," min"]}},"hours-ago-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"à l’instant","other":["il y a ",{"type":"idOrVar","name":"value"}," h"]}},"days-ago-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"aujourd’hui","other":["il y a ",{"type":"idOrVar","name":"value"}," j"]}},"minutes-until-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"maintenant","other":["dans ",{"type":"idOrVar","name":"value"}," min"]}},"hours-until-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"maintenant","other":["dans ",{"type":"idOrVar","name":"value"}," h"]}},"days-until-short":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"value"}]}],"value":{"zero":"aujourd’hui","other":["dans ",{"type":"idOrVar","name":"value"}," j"]}},"incorrectDate":"(date incorrecte)","dateTimeFormat_%c":"%a %e %b %Y %H:%M:%S","dateTimeFormat_%x":"%d/%m/%Y","dateTimeFormat_%X":"%H:%M:%S","shortTimeFormat":"%H:%M","shortTimeFormat12":"%I:%M %p","shortTimeFormat24":"%H:%M","shortDateTimeFormat":"%x %H:%M","weekStartsOnMonday":"1","firstDayOfTheWeek":"1","timePickerOrder":"HMp","datePickerOrder":"DMY","hourMinutesSeparator":":","listSeparator_middle":", ","time_am":"AM","time_pm":"PM","durationPattern":"hh:mm:ss.SS","brandShortName":"Vulpes","browserBrandShortName":"Vulpes","browserBrandFullName":"Project Vulpes — Firefox OS Community Project","ringtoneBrandName":"Par défaut","ringtoneSectionBrandName":"système","stopped_download_title":"Téléchargement arrêté","stopped_download_message":"Le téléchargement a été arrêté. Voulez-vous relancer le téléchargement ?","stopped_download_left_button":"Annuler","stopped_download_right_button":"Relancer","failed_download_title":"Le téléchargement a échoué","failed_download_message":{"value":["Le téléchargement de ",{"type":"idOrVar","name":"name"}," a échoué. Voulez-vous relancer le téléchargement ?"]},"failed_download_left_button":"Annuler","failed_download_right_button":"Relancer","stop_download_title":"Arrêter le téléchargement","stop_download_message":"Voulez-vous vraiment arrêter le téléchargement ?","stop_download_left_button":"Non","stop_download_right_button":"Oui","delete_download_title":"Suppression","delete_download_message":{"value":["Supprimer ",{"type":"idOrVar","name":"name"}," ?"]},"delete_download_left_button":"Annuler","delete_download_right_button":"Supprimer","delete_all_download_title":"Suppression","delete_all_download_left_button":"Annuler","delete_all_download_right_button":"Supprimer","delete_all_download_message":{"index":[{"type":"call","expr":{"type":"prop","expr":{"type":"glob","name":"cldr"},"prop":"plural","cmpt":false},"args":[{"type":"idOrVar","name":"number"}]}],"value":{"zero":"Ne supprimer aucun fichier ?","one":"Supprimer le fichier ?","two":["Supprimer ",{"type":"idOrVar","name":"number"}," fichiers ?"],"few":["Supprimer ",{"type":"idOrVar","name":"number"}," fichiers ?"],"many":["Supprimer ",{"type":"idOrVar","name":"number"}," fichiers ?"],"other":["Supprimer ",{"type":"idOrVar","name":"number"}," fichiers ?"]}},"unsupported_file_type_download_title":"Ouverture impossible","unsupported_file_type_download_message":{"value":["Aucune application n’est capable d’ouvrir ",{"type":"idOrVar","name":"name"},". Voulez-vous conserver le fichier ou le supprimer ?"]},"unsupported_file_type_download_left_button":"Conserver","unsupported_file_type_download_right_button":"Supprimer","file_not_found_download_title":"Fichier introuvable","file_not_found_download_message":"Le fichier est introuvable sur l’appareil","file_not_found_download_right_button":"OK","unmounted_sdcard_2_download_title":"Carte mémoire utilisée","unmounted_sdcard_2_download_message":"Veuillez débrancher le téléphone","unmounted_sdcard_2_download_right_button":"OK","no_provider_download_title":"Partage impossible","no_provider_download_message":"Aucun service disponible","no_provider_download_right_button":"OK","no_memory_download_title":"Carte SD pleine","no_memory_download_message":"Il n’y a plus assez de place sur votre carte SD pour enregistrer votre téléchargement. Veuillez supprimer quelques fichiers avant de relancer le téléchargement.","no_memory_download_right_button":"OK","no_sdcard_found_2_download_title":"Aucune carte mémoire","no_sdcard_found_2_download_message":"Insérez une carte mémoire ou sélectionnez dans les paramètres un autre emplacement par défaut pour les médias","no_sdcard_found_2_download_right_button":"OK","file_open_error_download_title":"Ouverture impossible","file_open_error_download_message":{"value":["Un problème est survenu lors de l’ouverture de ",{"type":"idOrVar","name":"name"},". Voulez-vous conserver le fichier ou le supprimer ?"]},"file_open_error_download_left_button":"Conserver","file_open_error_download_right_button":"Supprimer","restart_download_error":"Une erreur s’est produite lors de la reprise du téléchargement. Veuillez relancer le téléchargement.","no-downloads":"Aucun téléchargement","byteUnit-B":"o","byteUnit-KB":"Ko","byteUnit-MB":"Mo","byteUnit-GB":"Go","byteUnit-TB":"To","byteUnit-PB":"Po","byteUnit-EB":"Eo","byteUnit-ZB":"Zo","byteUnit-YB":"Yo","fileSize":{"value":[{"type":"idOrVar","name":"size"}," ",{"type":"idOrVar","name":"unit"}]},"percentage":{"value":[{"type":"idOrVar","name":"percentage"}," %"]},"partialResult":{"value":[{"type":"idOrVar","name":"partial"}," sur ",{"type":"idOrVar","name":"total"}]},"summary":{"value":[{"type":"idOrVar","name":"date"}," - ",{"type":"idOrVar","name":"status"}]},"download-stopped":"Arrêté","download-failed":"Le téléchargement a échoué","open_downloaded_file":"Ouvrir","share_downloaded_file":"Partager","wallpaper_downloaded_file":"Définir comme fond d’écran","ringtone_downloaded_file":"Définir comme sonnerie","cancel_downloaded_file":"Annuler","battery-charging-unavailable":"Durée de charge indisponible","battery-discharging-unavailable":"Temps restant indisponible"}
diff --git a/assets/webapps/settings.gaiamobile.org/manifest.webapp b/assets/webapps/settings.gaiamobile.org/manifest.webapp
index 60cdf691..c31ccbde 100755
--- a/assets/webapps/settings.gaiamobile.org/manifest.webapp
+++ b/assets/webapps/settings.gaiamobile.org/manifest.webapp
@@ -1 +1,119 @@
-{"name":"Settings","description":"Gaia Settings","type":"certified","launch_path":"/index.html#","developer":{"name":"The Gaia Team","url":"https://github.com/mozilla-b2g/gaia"},"permissions":{"themeable":{},"browser":{},"embed-apps":{},"mobileconnection":{},"voicemail":{},"bluetooth":{},"device-storage:pictures":{"access":"readonly"},"device-storage:music":{"access":"readonly"},"device-storage:videos":{"access":"readonly"},"device-storage:sdcard":{"access":"readwrite"},"device-storage:apps":{"access":"readonly"},"webapps-manage":{},"open-remote-window":{},"permissions":{},"systemXHR":{},"settings":{"access":"readwrite"},"wifi-manage":{},"time":{},"desktop-notification":{},"power":{},"idle":{},"telephony":{},"audio-channel-notification":{},"audio-channel-alarm":{},"downloads":{},"nfc":{},"sms":{},"firefox-accounts":{},"moz-extremely-unstable-and-will-change-webcomponents":{}},"activities":{"configure":{"filters":{"target":"device"},"href":"index.html#activity-configure","disposition":"inline","returnValue":true},"moz_configure_window":{"filters":{"target":"device"},"href":"index.html#moz-configure-window","disposition":"window"}},"connections":{"settingsstk":{"description":"Communicate between Settings app and System app for SIM Toolkit.","rules":{}}},"locales":{"ar":{"name":"الإعدادات","description":"الإعدادات"},"bg":{"name":"Настройки","description":"Gaia Настройки"},"bn-BD":{"name":"সেটিং","description":"গায়া সেটিং"},"ca":{"name":"Paràmetres","description":"Paràmetres del Gaia"},"cs":{"name":"Nastavení","description":"Nastavení systému Gaia"},"da":{"name":"Indstillinger","description":"Gaia indstillinger"},"de":{"name":"Einstellungen","description":"Einstellungen für Gaia"},"el":{"name":"Ρυθμίσεις","description":"Ρυθμίσεις του Gaia"},"en-US":{"name":"Settings","description":"Gaia Settings"},"es":{"name":"Ajustes","description":"Ajustes de Gaia"},"eu":{"name":"Ezarpenak","description":"Gaia ezarpenak"},"fr":{"name":"Paramètres","description":"Paramètres de Gaia"},"gl":{"name":"Axustes","description":"Axustes de Gaia"},"hr":{"name":"Postavke","description":"Gaia postavke"},"hu":{"name":"Beállítások","description":"Gaia beállítások"},"it":{"name":"Impostazioni","description":"Impostazioni Gaia"},"ja":{"name":"環境設定","description":"Gaia 設定"},"km":{"name":"ការកំណត់","description":"ការកំណត់ Gaia"},"ko":{"name":"설정","description":"Gaia 설정"},"lt":{"name":"Nuostatos","description":"„Gaia“ nuostatos"},"mk":{"name":"Поставки","description":"Поставки за Гаја"},"ms":{"name":"Tetapan","description":"Tetapan Gaia"},"ne-NP":{"name":"सेटिङहरू","description":"गाइया सेटिङहरू"},"nl":{"name":"Instellingen","description":"Gaia-instellingen"},"pa":{"name":"ਸੈਟਿੰਗ","description":"ਗੀਗਾ ਸੈਟਿੰਗ"},"pl":{"name":"Ustawienia","description":"Ustawienia Gaia"},"pt-BR":{"name":"Configurações","description":"Configurações do Gaia"},"ro":{"name":"Setări","description":"Setări Gaia"},"ru":{"name":"Параметры","description":"Параметры Gaia"},"sk":{"name":"Nastavenia","description":"Nastavenia Gaia"},"sq":{"name":"Rregullime","description":"Rregullimet Gaia"},"sr-Cyrl":{"name":"Поставке","description":"Gaia поставке"},"sr-Latn":{"name":"Postavke","description":"Gaia postavke"},"sv-SE":{"name":"Inställningar","description":"Gaia inställningar"},"tr":{"name":"Ayarlar","description":"Gaia Ayarları"},"zh-CN":{"name":"设置","description":"Gaia 设置"},"zh-TW":{"name":"設定","description":"Gaia 設定"}},"datastores-owned":{"homescreen_settings":{"access":"readwrite","description":"Stores home screen settings"}},"datastores-access":{"download_store":{"readonly":false,"description":"Stores download finished"},"vertical_preferences_store":{"readonly":false,"description":"Vertical homescreen preferences"}},"default_locale":"en-US","icons":{"84":"/style/icons/settings_84.png","126":"/style/icons/settings_126.png","142":"/style/icons/settings_142.png","189":"/style/icons/settings_189.png","284":"/style/icons/settings_284.png"},"orientation":"default","origin":"app://settings.gaiamobile.org","version":"2.5"}
\ No newline at end of file
+{
+ "name": "Settings",
+ "description": "Gaia Settings",
+ "type": "certified",
+ "launch_path": "/index.html#",
+ "developer": {
+ "name": "The Gaia Team",
+ "url": "https://github.com/mozilla-b2g/gaia"
+ },
+ "permissions": {
+ "themeable": {},
+ "browser": {},
+ "embed-apps": {},
+ "mobileconnection": {},
+ "voicemail": {},
+ "bluetooth": {},
+ "device-storage:pictures": {
+ "access": "readonly"
+ },
+ "device-storage:music": {
+ "access": "readonly"
+ },
+ "device-storage:videos": {
+ "access": "readonly"
+ },
+ "device-storage:sdcard": {
+ "access": "readwrite"
+ },
+ "device-storage:apps": {
+ "access": "readonly"
+ },
+ "webapps-manage": {},
+ "open-remote-window": {},
+ "permissions": {},
+ "systemXHR": {},
+ "settings": {
+ "access": "readwrite"
+ },
+ "wifi-manage": {},
+ "time": {},
+ "desktop-notification": {},
+ "power": {},
+ "idle": {},
+ "telephony": {},
+ "audio-channel-notification": {},
+ "audio-channel-alarm": {},
+ "downloads": {},
+ "nfc": {},
+ "sms": {},
+ "firefox-accounts": {},
+ "moz-extremely-unstable-and-will-change-webcomponents": {}
+ },
+ "activities": {
+ "configure": {
+ "filters": {
+ "target": "device"
+ },
+ "href": "index.html#activity-configure",
+ "disposition": "inline",
+ "returnValue": true
+ },
+ "moz_configure_window": {
+ "filters": {
+ "target": "device"
+ },
+ "href": "index.html#moz-configure-window",
+ "disposition": "window"
+ }
+ },
+ "connections": {
+ "settingsstk": {
+ "description": "Communicate between Settings app and System app for SIM Toolkit.",
+ "rules": {}
+ }
+ },
+ "locales": {
+ "ar": {
+ "name": "الضبط",
+ "description": "Gaia الضبط"
+ },
+ "en-US": {
+ "name": "Settings",
+ "description": "Gaia Settings"
+ },
+ "fr": {
+ "name": "Paramètres",
+ "description": "Paramètres de Gaia"
+ },
+ "zh-TW": {
+ "name": "設定",
+ "description": "Gaia 設定"
+ }
+ },
+ "datastores-owned": {
+ "homescreen_settings": {
+ "access": "readwrite",
+ "description": "Stores home screen settings"
+ }
+ },
+ "datastores-access": {
+ "download_store": {
+ "readonly": false,
+ "description": "Stores download finished"
+ },
+ "vertical_preferences_store": {
+ "readonly": false,
+ "description": "Vertical homescreen preferences"
+ }
+ },
+ "default_locale": "en-US",
+ "icons": {
+ "84": "/style/icons/settings_84.png",
+ "126": "/style/icons/settings_126.png",
+ "142": "/style/icons/settings_142.png",
+ "189": "/style/icons/settings_189.png",
+ "284": "/style/icons/settings_284.png"
+ },
+ "orientation": "default"
+}
diff --git a/assets/webapps/system.gaiamobile.org/js/app_chrome.js b/assets/webapps/system.gaiamobile.org/js/app_chrome.js
index a3e35090..b69d892e 100644
--- a/assets/webapps/system.gaiamobile.org/js/app_chrome.js
+++ b/assets/webapps/system.gaiamobile.org/js/app_chrome.js
@@ -475,20 +475,25 @@
}
}
- // Set the .icon property before saving for
- // backwards compatibility with verticalhome
+ // Set the .icon property before saving for backwards compatibility with
+ // verticalhome. A missing favicon must not prevent pinning the site.
+ var saveSite = function(icon) {
+ siteObject.icon = icon || '';
+ BookmarksDatabase.put(siteObject, siteObject.id)
+ .then(() => {
+ this.app.debug('pinSite: ' + siteObject.id);
+ this.systemBanner.show('site-pinned-to-home-screen');
+ })
+ .catch((error) => {
+ console.error('pinSite, Failed to pin site: ' + error);
+ });
+ }.bind(this);
+
IconsHelper.getIcon(siteObject.url, null,
- {icons: this.app.favicons}, siteObject).then(icon => {
- siteObject.icon = icon;
- BookmarksDatabase.put(siteObject, siteObject.id)
- .then(() => {
- this.app.debug('pinSite: ' + siteObject.id);
- this.systemBanner.show('site-pinned-to-home-screen');
- })
- .catch((error) => {
- console.error('pinSite, Failed to pin site: ' + error);
- });
- });
+ {icons: this.app.favicons}, siteObject).then(saveSite, function(error) {
+ this.app.debug('pinSite: using no favicon: ' + error);
+ saveSite('');
+ }.bind(this));
};
/**
@@ -986,12 +991,14 @@
return;
}
- // Enable/disable the bookmark option
- BookmarksDatabase.get(this._currentURL).then(function resolve(result) {
+ // start_url is the bookmark key for installed web apps, while _currentURL
+ // may be a later page in the app. Check the canonical key first.
+ var siteUrl = this.getSiteUrl();
+ BookmarksDatabase.get(siteUrl).then(function resolve(result) {
this.addToHomeButton.hidden = !!result;
}.bind(this),
function reject() {
- this.addToHomeButton.hidden = true;
+ this.addToHomeButton.hidden = false;
}.bind(this));
};
@@ -1282,7 +1289,13 @@
}
})
.catch((err) => {
- this.siteIcon.style.backgroundImage = '';
+ // Installed apps have a manifest icon even when the Places cache has
+ // no favicon. Keep the original chrome slot and select a crisp source.
+ const icons = this.app.manifest && this.app.manifest.icons;
+ const sizes = Object.keys(icons || {}).map(Number).filter(Number.isFinite).sort((a,b)=>a-b);
+ const size = sizes.find(n => n >= ICON_SIZE * devicePixelRatio) || sizes[sizes.length-1];
+ const icon = size && icons[size];
+ this.siteIcon.style.backgroundImage = icon ? 'url(' + JSON.stringify(new URL(icon, this.app.manifestURL).href) + ')' : '';
this.app.debug('setSiteIcon, error from getSiteIcon: %s', err);
});
};
diff --git a/assets/webapps/system.gaiamobile.org/js/quick_settings.js b/assets/webapps/system.gaiamobile.org/js/quick_settings.js
index bfaf65b7..67540ed1 100644
--- a/assets/webapps/system.gaiamobile.org/js/quick_settings.js
+++ b/assets/webapps/system.gaiamobile.org/js/quick_settings.js
@@ -346,13 +346,26 @@
case this.fullApp:
// XXX: This should be replaced probably by Web Activities
- var host = document.location.host;
- var domain = host.replace(/(^[\w\d]+\.)?([\w\d]+\.[a-z]+)/, '$2');
- var protocol = document.location.protocol + '//';
- applications.getByManifestURL(protocol + 'settings.' +
- domain + '/manifest.webapp').launch();
-
- UtilityTray.hide();
+ var settingsURL = new URL(document.location.href);
+ if (settingsURL.hostname.indexOf('system.') !== 0) {
+ console.error('Quick settings: cannot resolve Settings app host');
+ return;
+ }
+ settingsURL.hostname = 'settings.' +
+ settingsURL.hostname.slice('system.'.length);
+ settingsURL.pathname = '/manifest.webapp';
+ settingsURL.search = '';
+ settingsURL.hash = '';
+ applications.waitForReady().then(function() {
+ var settingsApp = applications.getByManifestURL(
+ settingsURL.href);
+ if (!settingsApp) {
+ console.error('Quick settings: Settings app is not installed');
+ return;
+ }
+ settingsApp.launch();
+ UtilityTray.hide();
+ });
break;
}
break;
diff --git a/assets/webapps/system.gaiamobile.org/style/system/initlogo.css b/assets/webapps/system.gaiamobile.org/style/system/initlogo.css
index 656f23d0..e996ac1f 100644
--- a/assets/webapps/system.gaiamobile.org/style/system/initlogo.css
+++ b/assets/webapps/system.gaiamobile.org/style/system/initlogo.css
@@ -57,5 +57,5 @@
#os-logo { background: #071d35 url('/shared/resources/branding/vulpes-wallpaper.png') center / cover; display:flex; flex-direction:column; align-items:center; justify-content:center; }
#os-logo::before { content:""; width:190px; height:190px; flex:none; background:url('/shared/resources/branding/vulpes-symbol.png') center / contain no-repeat; }
#os-logo::after { content:"Vulpes 2.7\A A community continuation of Firefox OS"; white-space:pre-line; padding:12px 22px; color:#f5f8fa; text-shadow:0 2px 8px #071d35; font:500 20px/1.8 sans-serif; text-align:center; }
-#poweroff-splash { background:#071d35 url('/shared/resources/branding/vulpes-symbol.png') center / 190px 190px no-repeat; }
+#poweroff-splash { background:#071d35; }
/* VULPES BRAND END */
diff --git a/assets/webapps/system.gaiamobile.org/style/system/system.css b/assets/webapps/system.gaiamobile.org/style/system/system.css
index 4fc39185..df9195c0 100644
--- a/assets/webapps/system.gaiamobile.org/style/system/system.css
+++ b/assets/webapps/system.gaiamobile.org/style/system/system.css
@@ -88,17 +88,17 @@ textarea,
#poweroff-ring-1 {
top: 25%;
- background-color: #e66600;
+ background-color: #36a9e8;
}
#poweroff-ring-2 {
top: 50%;
- background-color: #dc4e00;
+ background-color: #218bd1;
}
#poweroff-ring-3 {
top: 75%;
- background-color: #d24500;
+ background-color: #1473b8;
}
.poweroff-ring > span {
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 7aa6a9b6..db72bd4c 100644
--- a/assets/webapps/verticalhome.gaiamobile.org/gaia_build_defer_index.js
+++ b/assets/webapps/verticalhome.gaiamobile.org/gaia_build_defer_index.js
@@ -257,7 +257,7 @@ for(i in appIconsByManifestUrl){this.removeIconFromGrid(i);}
toAdd.forEach(function _toAdd(newApp){this.addIconToGrid(newApp.app);},this);app.itemStore.deferredSave(app.grid.getItems());},addIconToGrid:function(application){this.makeIcons(application);var appObject=this.mapToApp({manifestURL:application.manifestURL});app.grid.appendItemToExpandedGroup(appObject);app.grid.render();},removeIconFromGrid:function(manifestURL){var icons=app.grid.getIcons();var appObject=icons[manifestURL];if(appObject){appObject.removeFromGrid();}},populate:function(success){success(this.entries);},makeIcons:function(eachApp){var manifest=eachApp.manifest||eachApp.updateManifest;if(app.HIDDEN_ROLES.indexOf(manifest.role)!==-1){return;}
function eachIcon(icon){if(!icon.icon){return;}
icon.setPosition(this.store.getNextPosition());this.entries.push(icon);}
-this.entriesByManifestUrl[eachApp.manifestURL]=eachApp;if(manifest.entry_points){for(var i in manifest.entry_points){eachIcon.call(this,new GaiaGrid.Mozapp(eachApp,i));}}else{eachIcon.call(this,new GaiaGrid.Mozapp(eachApp));}},mapToApp:function(entry){var app=this.entriesByManifestUrl[entry.manifestURL];app=app||{manifestURL:entry.manifestURL,manifest:{name:'',icons:[]}};return new GaiaGrid.Mozapp(app,entry.entryPoint,{decoratedIconBlob:entry.decoratedIconBlob});},addPreviouslyInstalledSvApp:function(manifest){this.svPreviouslyInstalledApps.push({manifestURL:manifest});},isPreviouslyInstalled:function(manifest){for(var i=0,elemNum=this.svPreviouslyInstalledApps.length;i {
- li.setAttribute('aria-label', title);
+ var visibleTitle = title || fallbackTitle(option.title);
+ li.textContent = visibleTitle;
+ li.setAttribute('aria-label', visibleTitle);
});
} else {
li.textContent = option.title;
diff --git a/gaia/apps/camera/js/views/setting.js b/gaia/apps/camera/js/views/setting.js
index e83c9e49..1f2b1642 100755
--- a/gaia/apps/camera/js/views/setting.js
+++ b/gaia/apps/camera/js/views/setting.js
@@ -9,6 +9,25 @@ var debug = require('debug')('view:setting');
var bind = require('lib/bind');
var View = require('view');
+var settingFallbacks = {
+ 'self-timer': ['Self-Timer', 'Minuteur'],
+ 'self-timer-off': ['Off', 'Désactivé'],
+ 'self-timer-2-seconds': ['2 seconds', '2 secondes'],
+ 'self-timer-5-seconds': ['5 seconds', '5 secondes'],
+ 'self-timer-10-seconds': ['10 seconds', '10 secondes'],
+ grid: ['Grid Lines', 'Quadrillage'],
+ 'grid-off': ['Off', 'Désactivée'],
+ 'grid-on': ['On', 'Activé']
+};
+
+function fallbackText(key) {
+ var values = settingFallbacks[key];
+ if (!values) { return key; }
+ var locale = (document.documentElement.lang || navigator.language || 'en')
+ .toLowerCase();
+ return values[locale.indexOf('fr') === 0 ? 1 : 0];
+}
+
/**
* Exports
*/
@@ -50,6 +69,7 @@ module.exports = View.extend({
}
localizedValue.then(valueString => {
+ valueString = valueString || fallbackText(data.value);
this.l10n.setAttributes(
this.el,
'setting-option-' + data.title,
@@ -62,6 +82,15 @@ module.exports = View.extend({
valueText: data.optionsLocalizable === false ? data.value : '',
});
+ var title = this.el.querySelector('.setting_title');
+ if (title && !title.textContent) {
+ title.textContent = fallbackText(data.title);
+ }
+ var value = this.el.querySelector('.setting_value');
+ if (value && !value.textContent) {
+ value.textContent = valueString;
+ }
+
// Clean up
delete this.template;
diff --git a/gaia/apps/costcontrol/js/common.js b/gaia/apps/costcontrol/js/common.js
index 6408338e..75386db2 100755
--- a/gaia/apps/costcontrol/js/common.js
+++ b/gaia/apps/costcontrol/js/common.js
@@ -142,6 +142,11 @@ var Common = {
},
closeApplication: function() {
+ if (window.VulpesCompat) {
+ return window.VulpesCompat.call('apps.close', {}).catch(function(error) {
+ console.error('Unable to close Usage:', error);
+ });
+ }
return setTimeout(function _close() {
debug('Closing.');
window.close();
diff --git a/gaia/apps/homescreen/js/datastore.js b/gaia/apps/homescreen/js/datastore.js
index f2feab09..42521163 100755
--- a/gaia/apps/homescreen/js/datastore.js
+++ b/gaia/apps/homescreen/js/datastore.js
@@ -44,7 +44,13 @@
},
init: function() {
- var revisionString = localStorage.getItem(this.revisionName);
+ var revisionString;
+ try {
+ revisionString = localStorage.getItem(this.revisionName);
+ } catch (e) {
+ // The shell may deny partitioned localStorage; IndexedDB remains usable.
+ console.error('Homescreen datastore revision unavailable:', e);
+ }
if (revisionString) {
this.lastRevision = JSON.parse(revisionString);
}
@@ -187,8 +193,12 @@
updateRevision: function() {
this.lastRevision = this.datastore.revisionId;
- localStorage.setItem(this.revisionName,
- JSON.stringify(this.lastRevision));
+ try {
+ localStorage.setItem(this.revisionName,
+ JSON.stringify(this.lastRevision));
+ } catch (e) {
+ console.error('Homescreen datastore revision could not be saved:', e);
+ }
},
onChange: function(e) {
diff --git a/gaia/apps/homescreen/js/settings.js b/gaia/apps/homescreen/js/settings.js
index f944eeb4..b6f12fd0 100755
--- a/gaia/apps/homescreen/js/settings.js
+++ b/gaia/apps/homescreen/js/settings.js
@@ -82,7 +82,15 @@
}
// Restore existing settings
- var settingsString = localStorage.getItem('settings');
+ var settingsString;
+ try {
+ settingsString = localStorage.getItem('settings');
+ } catch (e) {
+ // Firefox partitions storage when Gaia is hosted in the desktop shell.
+ // Keep launching the home screen with defaults if that storage is denied.
+ console.error('Home screen local settings unavailable:', e);
+ return;
+ }
if (!settingsString) {
this.firstRun = true;
return;
@@ -105,11 +113,15 @@
Settings.prototype = {
save: function() {
- localStorage.setItem('settings', JSON.stringify({
- version: SETTINGS_VERSION,
- small: this.small,
- scrollSnapping: this.scrollSnapping
- }));
+ try {
+ localStorage.setItem('settings', JSON.stringify({
+ version: SETTINGS_VERSION,
+ small: this.small,
+ scrollSnapping: this.scrollSnapping
+ }));
+ } catch (e) {
+ console.error('Home screen settings could not be saved:', e);
+ }
}
};
diff --git a/gaia/apps/marketplace/marketplace.js b/gaia/apps/marketplace/marketplace.js
index b00ac828..bb7eee4f 100644
--- a/gaia/apps/marketplace/marketplace.js
+++ b/gaia/apps/marketplace/marketplace.js
@@ -12,7 +12,8 @@
'The catalogue is closed. You can reopen it below.'));
document.getElementById('open').hidden=true;
});
- function browse(){labels();message.hidden=true;dialog.hidden=true;frame.style.visibility='visible';frame.src='https://vulpes-os.org/marketplace/?mode=app&lang='+(fr()?'fr':'en');}
+ let catalogueUrl='';
+ function browse(reload=false){labels();message.hidden=true;dialog.hidden=true;frame.style.visibility='visible';const next='https://vulpes-os.org/marketplace/?mode=app&lang='+(fr()?'fr':'en');if(reload||!catalogueUrl){catalogueUrl=next;frame.src=next;}}
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;
@@ -22,7 +23,10 @@
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;}
+ }catch(error){if(current!==attempt)return;status(error.message==='PRIVILEGED_APP_UNSUPPORTED'?
+ text('Cette application demande des API privilégiées qui ne sont pas disponibles dans Vulpes OS.',
+ 'This app requires privileged APIs that are not available in Vulpes OS.'):
+ 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;}
@@ -32,7 +36,7 @@
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();});
+ addEventListener('languagechange',()=>{language=navigator.language;if(!busy)browse(true);});
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/search/manifest.webapp b/gaia/apps/search/manifest.webapp
index 53d85f90..7f374c4c 100755
--- a/gaia/apps/search/manifest.webapp
+++ b/gaia/apps/search/manifest.webapp
@@ -59,12 +59,12 @@
}
},
"icons": {
- "84": "/shared/resources/branding/firefox-browser.png",
- "126": "/shared/resources/branding/firefox-browser.png",
- "142": "/shared/resources/branding/firefox-browser.png",
- "189": "/shared/resources/branding/firefox-browser.png",
- "256": "/shared/resources/branding/firefox-browser.png",
- "284": "/shared/resources/branding/firefox-browser.png"
+ "84": "/shared/resources/branding/browser_84.png",
+ "126": "/shared/resources/branding/browser_126.png",
+ "142": "/shared/resources/branding/browser_142.png",
+ "189": "/shared/resources/branding/browser_189.png",
+ "256": "/shared/resources/branding/browser_256.png",
+ "284": "/shared/resources/branding/browser_284.png"
},
"chrome": {
"navigation": true
diff --git a/gaia/apps/search/shared/resources/branding/browser_126.png b/gaia/apps/search/shared/resources/branding/browser_126.png
new file mode 100644
index 00000000..905faf81
Binary files /dev/null and b/gaia/apps/search/shared/resources/branding/browser_126.png differ
diff --git a/gaia/apps/search/shared/resources/branding/browser_142.png b/gaia/apps/search/shared/resources/branding/browser_142.png
new file mode 100644
index 00000000..7a3346bf
Binary files /dev/null and b/gaia/apps/search/shared/resources/branding/browser_142.png differ
diff --git a/gaia/apps/search/shared/resources/branding/browser_189.png b/gaia/apps/search/shared/resources/branding/browser_189.png
new file mode 100644
index 00000000..7bfb5074
Binary files /dev/null and b/gaia/apps/search/shared/resources/branding/browser_189.png differ
diff --git a/gaia/apps/search/shared/resources/branding/browser_256.png b/gaia/apps/search/shared/resources/branding/browser_256.png
new file mode 100644
index 00000000..f665bfe1
Binary files /dev/null and b/gaia/apps/search/shared/resources/branding/browser_256.png differ
diff --git a/gaia/apps/search/shared/resources/branding/browser_284.png b/gaia/apps/search/shared/resources/branding/browser_284.png
new file mode 100644
index 00000000..21913992
Binary files /dev/null and b/gaia/apps/search/shared/resources/branding/browser_284.png differ
diff --git a/gaia/apps/search/shared/resources/branding/browser_336.png b/gaia/apps/search/shared/resources/branding/browser_336.png
new file mode 100644
index 00000000..3e15ea49
Binary files /dev/null and b/gaia/apps/search/shared/resources/branding/browser_336.png differ
diff --git a/gaia/apps/search/shared/resources/branding/browser_84.png b/gaia/apps/search/shared/resources/branding/browser_84.png
new file mode 100644
index 00000000..5089aa37
Binary files /dev/null and b/gaia/apps/search/shared/resources/branding/browser_84.png differ
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 7e01cc98..02f0d0f9 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
@@ -290,8 +290,12 @@ define(function(require) {
*/
_loadApps: function pl__loadApps(apps) {
var table = this._permissionTable;
+ var internalRoles = ['system', 'input', 'homescreen', 'search', 'addon', 'langpack'];
apps.forEach(function(app) {
var manifest = app.manifest ? app.manifest : app.updateManifest;
+ if (manifest.role && internalRoles.indexOf(manifest.role) !== -1) {
+ return;
+ }
if (manifest.type != 'certified') {
this._apps.push(app);
return;
diff --git a/gaia/apps/settings/js/panels/battery/panel.js b/gaia/apps/settings/js/panels/battery/panel.js
index c23e1034..b346536d 100755
--- a/gaia/apps/settings/js/panels/battery/panel.js
+++ b/gaia/apps/settings/js/panels/battery/panel.js
@@ -31,6 +31,8 @@ define(function(require) {
}
} else if (time !== Infinity && time === 0) {
l10nAttrs.id = 'battery-' + type + '-complete';
+ } else {
+ l10nAttrs.id = 'battery-' + type + '-unavailable';
}
return l10nAttrs;
diff --git a/gaia/apps/settings/locales/settings.en-US.properties b/gaia/apps/settings/locales/settings.en-US.properties
index 15a4a44f..c2c372aa 100755
--- a/gaia/apps/settings/locales/settings.en-US.properties
+++ b/gaia/apps/settings/locales/settings.en-US.properties
@@ -1356,6 +1356,8 @@ battery-level-percent={{level}}%
battery-current-level=Current Level ({{level}}%)
battery-charging-calculating=Calculating time required…
battery-discharging-calculating=Calculating time remaining…
+battery-charging-unavailable=Charge time unavailable
+battery-discharging-unavailable=Time remaining unavailable
battery-charging-complete=Fully charged
battery-discharging-complete=Fully depleted
battery-time-hour={[ plural(hours) ]}
diff --git a/gaia/apps/system/js/app_chrome.js b/gaia/apps/system/js/app_chrome.js
index a3e35090..a1027207 100755
--- a/gaia/apps/system/js/app_chrome.js
+++ b/gaia/apps/system/js/app_chrome.js
@@ -475,20 +475,25 @@
}
}
- // Set the .icon property before saving for
- // backwards compatibility with verticalhome
+ // Set the .icon property before saving for backwards compatibility with
+ // verticalhome. A missing favicon must not prevent pinning the site.
+ var saveSite = function(icon) {
+ siteObject.icon = icon || '';
+ BookmarksDatabase.put(siteObject, siteObject.id)
+ .then(() => {
+ this.app.debug('pinSite: ' + siteObject.id);
+ this.systemBanner.show('site-pinned-to-home-screen');
+ })
+ .catch((error) => {
+ console.error('pinSite, Failed to pin site: ' + error);
+ });
+ }.bind(this);
+
IconsHelper.getIcon(siteObject.url, null,
- {icons: this.app.favicons}, siteObject).then(icon => {
- siteObject.icon = icon;
- BookmarksDatabase.put(siteObject, siteObject.id)
- .then(() => {
- this.app.debug('pinSite: ' + siteObject.id);
- this.systemBanner.show('site-pinned-to-home-screen');
- })
- .catch((error) => {
- console.error('pinSite, Failed to pin site: ' + error);
- });
- });
+ {icons: this.app.favicons}, siteObject).then(saveSite, function(error) {
+ this.app.debug('pinSite: using no favicon: ' + error);
+ saveSite('');
+ }.bind(this));
};
/**
@@ -986,12 +991,14 @@
return;
}
- // Enable/disable the bookmark option
- BookmarksDatabase.get(this._currentURL).then(function resolve(result) {
+ // start_url is the bookmark key for installed web apps, while _currentURL
+ // may be a later page in the app. Check the canonical key first.
+ var siteUrl = this.getSiteUrl();
+ BookmarksDatabase.get(siteUrl).then(function resolve(result) {
this.addToHomeButton.hidden = !!result;
}.bind(this),
function reject() {
- this.addToHomeButton.hidden = true;
+ this.addToHomeButton.hidden = false;
}.bind(this));
};
diff --git a/gaia/apps/system/js/cover_screen.js b/gaia/apps/system/js/cover_screen.js
index ce41a5ff..85ddb303 100755
--- a/gaia/apps/system/js/cover_screen.js
+++ b/gaia/apps/system/js/cover_screen.js
@@ -242,76 +242,29 @@
div.dataset.zIndexLevel = 'poweroff-splash';
div.id = 'poweroff-splash';
- this.poweroffLogoLoader = new LogoLoader(this._poweroff);
+ // Vulpes uses Gaia's three-ring shutdown animation without a carrier
+ // image or video layered over it.
+ var inner;
+ for (var i = 1; i <= 3; i++) {
+ var outer = document.createElement('span');
+ outer.className = 'poweroff-ring';
+ outer.id = 'poweroff-ring-' + i;
+ div.appendChild(outer);
+ inner = document.createElement('span');
+ outer.appendChild(inner);
+ }
- this.poweroffLogoLoader.onload = function(elem) {
- // Perform customized animation.
- div.appendChild(elem);
- div.className = 'step1';
-
- if (elem.tagName.toLowerCase() == 'video' && !elem.ended) {
- elem.onended = function() {
- elem.classList.add('hide');
- // XXX workaround of bug 831747
- // Unload the video. This releases the video decoding hardware
- // so other apps can use it.
- elem.removeAttribute('src');
- elem.load();
- };
- elem.play();
- } else {
- div.addEventListener('animationend', function() {
- elem.classList.add('hide');
- if (elem.tagName.toLowerCase() == 'video') {
- // XXX workaround of bug 831747
- // Unload the video. This releases the video decoding hardware
- // so other apps can use it.
- elem.removeAttribute('src');
- elem.load();
- }
- });
+ div.className = 'step1';
+ var nextAnimation = function nextAnimation(e) {
+ if (e.target == div) {
+ div.className = 'step2';
}
-
- elem.addEventListener('transitionend', function() {
+ if (e.target == inner) {
self._actualPowerOff(reboot);
- });
- document.getElementById('screen').appendChild(div);
- };
-
- this.poweroffLogoLoader.onnotfound = function() {
- // - Perform OS default animation.
-
- // The overall animation ends when the inner span of the bottom ring
- // is animated, so we store it for detecting.
- var inner;
-
- for (var i = 1; i <= 3; i++) {
- var outer = document.createElement('span');
- outer.className = 'poweroff-ring';
- outer.id = 'poweroff-ring-' + i;
- div.appendChild(outer);
-
- inner = document.createElement('span');
- outer.appendChild(inner);
}
-
- div.className = 'step1';
- var nextAnimation = function nextAnimation(e) {
- // Switch to next class
- if (e.target == div) {
- div.className = 'step2';
- }
-
- if (e.target != inner) {
- return;
- }
-
- self._actualPowerOff(reboot);
- };
- div.addEventListener('animationend', nextAnimation);
-
- document.getElementById('screen').appendChild(div);
};
+ div.addEventListener('animationend', nextAnimation);
+ document.getElementById('screen').appendChild(div);
},
/**
diff --git a/gaia/apps/system/js/lockscreen_window.js b/gaia/apps/system/js/lockscreen_window.js
index 47cac330..50029035 100755
--- a/gaia/apps/system/js/lockscreen_window.js
+++ b/gaia/apps/system/js/lockscreen_window.js
@@ -265,7 +265,8 @@
// until it works. It may fail at the moment the screenchange
// event has been fired, so we may need to try it several times.
var tryLockOrientation = () => {
- if (screen.mozLockOrientation('portrait-primary')) {
+ // Allow the lock screen to follow the physical display orientation.
+ if (screen.mozLockOrientation('any')) {
if (!this.orientationLockID) {
throw new Error('No orientation ID. This function should only' +
'be invoked as a interval callback');
diff --git a/gaia/apps/system/js/quick_settings.js b/gaia/apps/system/js/quick_settings.js
index bfaf65b7..67540ed1 100755
--- a/gaia/apps/system/js/quick_settings.js
+++ b/gaia/apps/system/js/quick_settings.js
@@ -346,13 +346,26 @@
case this.fullApp:
// XXX: This should be replaced probably by Web Activities
- var host = document.location.host;
- var domain = host.replace(/(^[\w\d]+\.)?([\w\d]+\.[a-z]+)/, '$2');
- var protocol = document.location.protocol + '//';
- applications.getByManifestURL(protocol + 'settings.' +
- domain + '/manifest.webapp').launch();
-
- UtilityTray.hide();
+ var settingsURL = new URL(document.location.href);
+ if (settingsURL.hostname.indexOf('system.') !== 0) {
+ console.error('Quick settings: cannot resolve Settings app host');
+ return;
+ }
+ settingsURL.hostname = 'settings.' +
+ settingsURL.hostname.slice('system.'.length);
+ settingsURL.pathname = '/manifest.webapp';
+ settingsURL.search = '';
+ settingsURL.hash = '';
+ applications.waitForReady().then(function() {
+ var settingsApp = applications.getByManifestURL(
+ settingsURL.href);
+ if (!settingsApp) {
+ console.error('Quick settings: Settings app is not installed');
+ return;
+ }
+ settingsApp.launch();
+ UtilityTray.hide();
+ });
break;
}
break;
diff --git a/gaia/apps/system/js/wrapper_factory.js b/gaia/apps/system/js/wrapper_factory.js
index 238afeea..a7056777 100755
--- a/gaia/apps/system/js/wrapper_factory.js
+++ b/gaia/apps/system/js/wrapper_factory.js
@@ -187,6 +187,15 @@
},
hasPermission: function wf_hasPermission(app, permission) {
+ // The embedded Gaia browser is the trusted system web client. Newer
+ // Gecko builds no longer expose mozPermissionSettings to this code, so
+ // honour its declared window permission and keep popup navigation in
+ // Gaia instead of letting Gecko open a desktop browser window.
+ if (app && app.manifest && app.manifest.role === 'search' &&
+ permission === 'open-remote-window' && app.manifest.permissions &&
+ app.manifest.permissions['open-remote-window']) {
+ return true;
+ }
var mozPerms = navigator.mozPermissionSettings;
if (!mozPerms) {
return false;
diff --git a/gaia/apps/system/style/app_install_manager/app_install_manager.css b/gaia/apps/system/style/app_install_manager/app_install_manager.css
index f56de7d6..320af8d0 100755
--- a/gaia/apps/system/style/app_install_manager/app_install_manager.css
+++ b/gaia/apps/system/style/app_install_manager/app_install_manager.css
@@ -70,7 +70,7 @@
text-overflow: ellipsis;
}
#app-install-dialog p {
- font-size: 1.6rem;
+ font-size: 1.4rem;
font-weight: 400;
line-height: normal;
border-top: none;
@@ -79,7 +79,7 @@
margin-top: 0;
}
#app-install-dialog span {
- font-size: 1.6rem;
+ font-size: 1.4rem;
font-weight: 400;
line-height: normal;
color: inherit;
diff --git a/gaia/apps/system/style/system/initlogo.css b/gaia/apps/system/style/system/initlogo.css
index 7943c17e..a350c2ac 100755
--- a/gaia/apps/system/style/system/initlogo.css
+++ b/gaia/apps/system/style/system/initlogo.css
@@ -57,6 +57,6 @@
#os-logo { background:#071d35; display:flex; flex-direction:column; align-items:center; justify-content:center; }
#os-logo::before { content:""; width:min(42vw,190px); aspect-ratio:1; flex:none; background:url('/shared/resources/branding/vulpes-symbol.png') center / contain no-repeat; animation:vulpes-boot 2s ease-in-out infinite alternate; }
#os-logo::after { content:"VULPES\A Firefox OS Community Project"; white-space:pre-line; padding:12px 22px; color:#f5f8fa; font:500 20px/1.8 FiraSans,sans-serif; text-align:center; }
-#poweroff-splash { background:#071d35 url('/shared/resources/branding/vulpes-symbol.png') center / 190px 190px no-repeat; }
+#poweroff-splash { background:#071d35; }
@keyframes vulpes-boot { from {opacity:.75} to {opacity:1} }
@media (prefers-reduced-motion: reduce) { #os-logo::before {animation:none} }
diff --git a/gaia/apps/system/test/unit/cover_screen_test.js b/gaia/apps/system/test/unit/cover_screen_test.js
index 8a545d73..bca868cd 100755
--- a/gaia/apps/system/test/unit/cover_screen_test.js
+++ b/gaia/apps/system/test/unit/cover_screen_test.js
@@ -11,7 +11,7 @@ requireApp('system/shared/test/unit/mocks/mock_navigator_moz_settings.js');
requireApp('system/shared/test/unit/mocks/mock_navigator_moz_power.js');
suite('CoverScreen >', function() {
- var settingsCore, realSettings, subject, realMozPower;
+ var settingsCore, realSettings, subject, realMozPower, fakeScreen;
const DEFAULT_VIDEO_ON = '/resources/power/carrier_power_on.mp4';
const DEFAULT_VIDEO_OFF = '/resources/power/carrier_power_off.mp4';
const DEFAULT_IMAGE_ON = '/resources/power/carrier_power_on.png';
@@ -113,6 +113,9 @@ suite('CoverScreen >', function() {
var fakeElement = document.createElement('div');
if (id === 'poweroff-splash') {
return null;
+ } else if (id === 'screen') {
+ fakeScreen = fakeElement;
+ return fakeScreen;
} else {
return fakeElement.cloneNode(true);
}
@@ -135,10 +138,9 @@ suite('CoverScreen >', function() {
test('restart requested', function() {
var stub = this.sinon.stub(navigator.mozPower, 'reboot');
subject.poweroff(true);
- var element = document.createElement('div');
- var transitionStub = this.sinon.stub(element, 'addEventListener');
- subject.poweroffLogoLoader.onload(element);
- transitionStub.getCall(0).args[1]();
+ fakeScreen.lastChild.dispatchEvent(new Event('animationend'));
+ fakeScreen.lastChild.querySelector('#poweroff-ring-3 > span')
+ .dispatchEvent(new Event('animationend', { bubbles: true }));
assert.ok(stub.calledOnce);
});
@@ -151,11 +153,9 @@ suite('CoverScreen >', function() {
test('poweroff requested', function() {
var stub = this.sinon.stub(navigator.mozPower, 'powerOff');
subject.poweroff(false);
-
- var element = document.createElement('div');
- var transitionStub = this.sinon.stub(element, 'addEventListener');
- subject.poweroffLogoLoader.onload(element);
- transitionStub.getCall(0).args[1]();
+ fakeScreen.lastChild.dispatchEvent(new Event('animationend'));
+ fakeScreen.lastChild.querySelector('#poweroff-ring-3 > span')
+ .dispatchEvent(new Event('animationend', { bubbles: true }));
assert.ok(stub.calledOnce);
});
diff --git a/gaia/apps/verticalhome/js/sources/application.js b/gaia/apps/verticalhome/js/sources/application.js
index 85cd60da..6465d335 100755
--- a/gaia/apps/verticalhome/js/sources/application.js
+++ b/gaia/apps/verticalhome/js/sources/application.js
@@ -253,9 +253,15 @@
}
};
+ // The Browser artwork changed with Vulpes branding. Don't keep its
+ // decorated icon from an older homescreen database; let mozApps return
+ // the manifest icon at the current grid's best available resolution.
+ var isVulpesBrowser = /^(?:app:\/\/search\.gaiamobile\.org|http:\/\/search\.localhost(?::\d+)?)\/manifest\.webapp$/
+ .test(entry.manifestURL);
+
return new GaiaGrid.Mozapp(app, entry.entryPoint, {
// cached decorated icon blob to load faster
- decoratedIconBlob: entry.decoratedIconBlob
+ decoratedIconBlob: isVulpesBrowser ? null : entry.decoratedIconBlob
});
},
diff --git a/gaia/shared/resources/branding/unofficial/browser_126.png b/gaia/shared/resources/branding/unofficial/browser_126.png
index eef767da..905faf81 100755
Binary files a/gaia/shared/resources/branding/unofficial/browser_126.png and b/gaia/shared/resources/branding/unofficial/browser_126.png differ
diff --git a/gaia/shared/resources/branding/unofficial/browser_142.png b/gaia/shared/resources/branding/unofficial/browser_142.png
index eef767da..7a3346bf 100755
Binary files a/gaia/shared/resources/branding/unofficial/browser_142.png and b/gaia/shared/resources/branding/unofficial/browser_142.png differ
diff --git a/gaia/shared/resources/branding/unofficial/browser_189.png b/gaia/shared/resources/branding/unofficial/browser_189.png
index eef767da..7bfb5074 100755
Binary files a/gaia/shared/resources/branding/unofficial/browser_189.png and b/gaia/shared/resources/branding/unofficial/browser_189.png differ
diff --git a/gaia/shared/resources/branding/unofficial/browser_284.png b/gaia/shared/resources/branding/unofficial/browser_284.png
index eef767da..21913992 100755
Binary files a/gaia/shared/resources/branding/unofficial/browser_284.png and b/gaia/shared/resources/branding/unofficial/browser_284.png differ
diff --git a/gaia/shared/resources/branding/unofficial/browser_84.png b/gaia/shared/resources/branding/unofficial/browser_84.png
index eef767da..5089aa37 100755
Binary files a/gaia/shared/resources/branding/unofficial/browser_84.png and b/gaia/shared/resources/branding/unofficial/browser_84.png differ
diff --git a/gaia/shared/style/value_selector.css b/gaia/shared/style/value_selector.css
index 2350f698..cc906501 100755
--- a/gaia/shared/style/value_selector.css
+++ b/gaia/shared/style/value_selector.css
@@ -54,7 +54,7 @@
position: relative;
font-weight: 300;
font-size: 2.2rem;
- line-height: 3.9rem;
+ line-height: normal;
color: #fff;
transition: background-color 0.2s ease;
}
@@ -81,8 +81,10 @@ html[dir="rtl"] [role="dialog"][data-type="value-selector"] [role="listbox"] li
[role="dialog"][data-type="value-selector"] [role="listbox"] li label span {
display: block;
- padding: 1rem 1.5rem;
- line-height: 4rem;
+ box-sizing: border-box;
+ min-height: 5.2rem;
+ padding: 1.2rem 1.5rem;
+ line-height: 2.8rem;
word-wrap: break-word;
}
diff --git a/host/NativeCamera.sys.mjs b/host/NativeCamera.sys.mjs
index e4e27f8a..c93d3e3b 100644
--- a/host/NativeCamera.sys.mjs
+++ b/host/NativeCamera.sys.mjs
@@ -3,7 +3,14 @@ import {Subprocess} from 'resource://gre/modules/Subprocess.sys.mjs';
import {setTimeout, clearTimeout} from 'resource://gre/modules/Timer.sys.mjs';
export class NativeCamera {
- constructor() { this.session = null; this.queue = Promise.resolve(); this.closing = Promise.resolve(); this.nextSession = 0; }
+ constructor(device = '') {
+ this.device = device;
+ this.previewSize = device === 'beatles' ? {width:320,height:240} : {width:640,height:480};
+ this.session = null;
+ this.queue = Promise.resolve();
+ this.closing = Promise.resolve();
+ this.nextSession = 0;
+ }
request(actor, params) {
const work = this.queue.then(() => this.perform(actor, params));
@@ -24,7 +31,8 @@ export class NativeCamera {
if(length>65536) throw Error('CAMERA_INVALID_FORMAT');
return pipe.readJSON(length);
}
- if(length!==0 && length!==640*480*3/2) throw Error('CAMERA_INVALID_FRAME');
+ const expected = this.session.width * this.session.height * 3 / 2;
+ if(length!==0 && length!==expected) throw Error('CAMERA_INVALID_FRAME');
return {sequence,data:length ? await pipe.read(length) : null};
})(),
new Promise((_, reject) => { timer=setTimeout(()=>reject(Error('CAMERA_TIMEOUT')),15000); }),
@@ -40,15 +48,28 @@ export class NativeCamera {
if (params.action === 'start') {
if (this.session && this.session.actor !== actor) throw Error('CAMERA_BUSY');
await this.close();
- const process = await Subprocess.call({command:'/usr/bin/python3',
- arguments:['-u', '/etc/tundra/camera-preview.py', '--camera', params.camera === 'front' ? '1' : '0'],
+ if (this.device === 'beatles' && params.camera === 'front') throw Error('CAMERA_UNAVAILABLE');
+ const beatles = this.device === 'beatles';
+ const process = await Subprocess.call({command:beatles?'/usr/bin/python3.11':'/usr/bin/python3',
+ arguments:beatles
+ ? ['-u','/opt/vulpes/tundra/runtime/beatles-camera-client.py']
+ : ['-u', '/etc/tundra/camera-preview.py', '--camera', params.camera === 'front' ? '1' : '0',
+ '--width',String(this.previewSize.width),'--height',String(this.previewSize.height)],
stderr:'ignore'});
this.session = {actor, process, id: ++this.nextSession};
try {
const metadata = await this.packet(this.session, true);
- if (!metadata.ready || metadata.format !== 'nv21' || metadata.width !== 640 || metadata.height !== 480)
+ if (!metadata.ready || metadata.format !== 'nv21' ||
+ metadata.width !== this.previewSize.width || metadata.height !== this.previewSize.height)
throw Error('CAMERA_INVALID_FORMAT');
- return {...metadata, session: this.session.id};
+ this.session.width = metadata.width;
+ this.session.height = metadata.height;
+ return {...metadata, session: this.session.id,
+ // The 2720 Flip has 400 MB of RAM and copies each frame through
+ // CameraService, Gecko IPC and a software canvas. Polling at the
+ // sensor's nominal rate exhausts its zram; keep its preview bounded.
+ frameRate: beatles ? 5 : 30,
+ cameras: beatles ? ['back'] : ['back','front']};
} catch (error) { this.close(); throw error; }
}
if (!this.session || this.session.actor !== actor || this.session.id !== params.session)
diff --git a/host/Platform.sys.mjs b/host/Platform.sys.mjs
index 9cd7a3af..94ed1f37 100644
--- a/host/Platform.sys.mjs
+++ b/host/Platform.sys.mjs
@@ -46,7 +46,7 @@ export class Platform {
this.notifications = new Map();
this.nextId = 1;
this.cameraWindow = null;
- this.nativeCamera = new NativeCamera();
+ this.nativeCamera = new NativeCamera(Services.env.get('VULPES_DEVICE') || '');
this.picking = false;
}
async init() {
@@ -242,6 +242,7 @@ export class Platform {
throw Error('NOT_SUPPORTED');
}
if (Services.env.get('VULPES_TUNDRA_STATUS') === '1') {
+ if (operation === 'cameraPreview') return this.nativeCamera.request(actor, params);
if (operation === 'wifi') return tundraWifi(params);
const radio = Services.env.get('VULPES_TUNDRA_RADIO') === '1';
if (radio && operation === 'radioPower') return tundraRadio('radioPower',params);
@@ -255,7 +256,7 @@ export class Platform {
adapter:{version:'0.1.0-dev',device:'beatles'}, tundra:'0.1.0-dev',
version:this.runtime.release.version, gaia:this.runtime.release.gaia,
gecko:Services.appinfo.platformVersion, hardwareStatus:true,
- radioStatus:true, wifiControl:Services.env.get('VULPES_TUNDRA_WIFI') === '1', camera:false, media:true, contacts:true,
+ radioStatus:true, wifiControl:Services.env.get('VULPES_TUNDRA_WIFI') === '1', camera:Services.env.get('VULPES_TUNDRA_CAMERA') === '1', media:true, contacts:true,
notifications:true, dial:radio, callControl:radio, sms:radio, mms:false, callAudio:radio, alarms:false,
};
}
diff --git a/host/Runtime.sys.mjs b/host/Runtime.sys.mjs
index 48bf6a76..f652a129 100644
--- a/host/Runtime.sys.mjs
+++ b/host/Runtime.sys.mjs
@@ -281,6 +281,16 @@ export const Runtime = {
this.actors.add(actor);
const { method, params } = request;
if (typeof method !== 'string') return { error: { code: 'INVALID_REQUEST' } };
+ if (method === 'system.screenshot') {
+ if (app.id !== 'system') return { error: { code: 'PERMISSION_DENIED' } };
+ try { return { result: await Views.captureSystem(actor) }; }
+ catch (error) { return { error: { code: error.message || 'SCREENSHOT_FAILED' } }; }
+ }
+ if (method === 'apps.close') {
+ if (app.id !== 'costcontrol') return { error: { code: 'PERMISSION_DENIED' } };
+ this.broadcast('home', {}, 'system');
+ return { result: true };
+ }
if(method.startsWith('marketplace.')) {
if(app.id!=='marketplace')return {error:{code:'PERMISSION_DENIED'}};
try {
diff --git a/host/TicketRegressionAudit.sys.mjs b/host/TicketRegressionAudit.sys.mjs
index fab194e4..712e22fa 100644
--- a/host/TicketRegressionAudit.sys.mjs
+++ b/host/TicketRegressionAudit.sys.mjs
@@ -13,7 +13,7 @@ export async function auditTickets({win,evaluate,wait,tap,sleep,root}) {
await sleep(600);
};
win.focus();win.document.getElementById('system').focus();
- await step('unlock',()=>ev('system','ScreenManager.turnScreenOn(true);if(Service.query("locked"))await Service.request("unlock",{forcibly:true});return true;'));
+ await step('unlock',()=>ev('system','ScreenManager.turnScreenOn(true);if(Service.query("locked"))lockScreen.unlock(true);return !Service.query("locked");'));
await step('cameraOpen',async()=>{
await launch('camera');
await wait('camera','return !!app.camera.mozCamera && !app.camera.isBusy;',15000);
diff --git a/host/UnixControl.sys.mjs b/host/UnixControl.sys.mjs
index 4deb30c5..b05710c0 100644
--- a/host/UnixControl.sys.mjs
+++ b/host/UnixControl.sys.mjs
@@ -19,9 +19,10 @@ export function exchange(path, params, limit=65536, timeout=20000) {
const bytes=new TextEncoder().encode(JSON.stringify(params)+'\n');
if (bytes.length>1228800) return Promise.reject(Error('REQUEST_TOO_LARGE'));
return new Promise((resolve,reject)=>{
- let transport,input,output,reader,timer,finished=false,received='',offset=0;
+ let transport,input,output,reader,pump,timer,finished=false,received='',offset=0;
const finish=(error,value)=>{
if(finished)return;finished=true;clearTimeout(timer);
+ try{pump?.cancel(Cr.NS_BINDING_ABORTED);}catch(_){}
try{input?.close();}catch(_){}try{output?.close();}catch(_){}
try{transport?.close(Cr.NS_BINDING_ABORTED);}catch(_){}
if(error)reject(error);else resolve(value);
@@ -35,20 +36,31 @@ export function exchange(path, params, limit=65536, timeout=20000) {
output=transport.openOutputStream(0,0,0).QueryInterface(Ci.nsIAsyncOutputStream);
reader=Cc['@mozilla.org/scriptableinputstream;1'].createInstance(Ci.nsIScriptableInputStream);
reader.init(input);
- const read={onInputStreamReady(){
- if(finished)return;
- try{
- const count=input.available();
- if(!count)throw Error('BROKER_CLOSED');
- if(received.length+count>limit)throw Error('BROKER_REPLY_TOO_LARGE');
- received+=reader.readBytes(count);
+ const read={
+ onStartRequest() {},
+ onDataAvailable(_request,stream,_offset,count){
+ if(finished)return;
+ try{
+ if(received.length+count>limit)throw Error('BROKER_REPLY_TOO_LARGE');
+ received+=reader.readBytes(count);
+ const end=received.indexOf('\n');
+ if(end>=0){
+ const data=Uint8Array.from(received.slice(0,end),c=>c.charCodeAt(0));
+ finish(null,JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(data)));
+ }
+ }catch(error){finish(error);}
+ },
+ onStopRequest(_request,status){
+ if(finished)return;
const end=received.indexOf('\n');
if(end>=0){
- const data=Uint8Array.from(received.slice(0,end),c=>c.charCodeAt(0));
- finish(null,JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(data)));
- }else input.asyncWait(read,0,0,Services.tm.currentThread);
- }catch(error){finish(error);}
- }};
+ try{
+ const data=Uint8Array.from(received.slice(0,end),c=>c.charCodeAt(0));
+ finish(null,JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(data)));
+ }catch(error){finish(error);}
+ }else finish(Error(status===Cr.NS_OK?'BROKER_CLOSED':'BROKER_READ_FAILED'));
+ }
+ };
const write={onOutputStreamReady(){
if(finished)return;
try{
@@ -57,7 +69,13 @@ export function exchange(path, params, limit=65536, timeout=20000) {
if(offset {
+ canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error('SCREENSHOT_FAILED')), 'image/png');
+ });
+ },
syncContext(entry) {
const context=entry.browser.browsingContext;
if (!context || context.id===entry.topContextId) return;
diff --git a/host/actors/KeypadChild.sys.mjs b/host/actors/KeypadChild.sys.mjs
index e8480f2f..f5264397 100644
--- a/host/actors/KeypadChild.sys.mjs
+++ b/host/actors/KeypadChild.sys.mjs
@@ -5,14 +5,7 @@ import {InputEditor} from 'resource://vulpes/services/input-editor.mjs';
export class KeypadChild extends JSWindowActorChild {
receiveMessage(message) {
if (message.name === 'Vulpes:FocusBrowserToolbar') {
- if (this.document.location.hostname !== 'system.localhost') return;
- const bar = [...this.document.querySelectorAll('.chrome .urlbar')].find(element => {
- if (!element.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})) return false;
- const rect = element.getBoundingClientRect();
- return rect.width > 0 && element.contains(this.document.elementFromPoint(
- rect.x + rect.width / 2, rect.y + rect.height / 2));
- });
- if (bar) this.select(bar);
+ this.focusBrowserToolbar();
return;
}
if (message.name !== 'Vulpes:Navigate') return;
@@ -37,6 +30,13 @@ export class KeypadChild extends JSWindowActorChild {
this.hashStartReturnMode=this.composer.returnMode;
}
const target = document.activeElement;
+ // The collapsed Gaia rocketbar can leave its search input focused after
+ // dismissing address entry. On keypad devices, Up should still reopen the
+ // browser toolbar instead of moving the caret in that hidden input.
+ if (key === 'ArrowUp' && document.location.hostname === 'system.localhost' &&
+ target?.id === 'rocketbar-input' && !document.querySelector('#rocketbar')?.classList.contains('active')) {
+ if (this.focusBrowserToolbar()) return;
+ }
const editing = !target?.readOnly && (target?.isContentEditable || target?.localName === 'textarea' ||
(target?.localName === 'input' && ['text','search','email','url','tel','password','number'].includes(target.type)));
const modal = [...document.querySelectorAll('[aria-modal="true"],[data-type="confirm"],[data-type="action"]')]
@@ -172,6 +172,18 @@ export class KeypadChild extends JSWindowActorChild {
}
this.select(elements[index]);
}
+ focusBrowserToolbar() {
+ if (this.document.location.hostname !== 'system.localhost') return false;
+ const bar = [...this.document.querySelectorAll('.chrome .urlbar')].find(element => {
+ if (!element.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})) return false;
+ const rect = element.getBoundingClientRect();
+ return rect.width > 0 && element.contains(this.document.elementFromPoint(
+ rect.x + rect.width / 2, rect.y + rect.height / 2));
+ });
+ if (!bar) return false;
+ this.select(bar);
+ return true;
+ }
select(element) {
const document = this.document;
if (!this.style) {
diff --git a/host/bootstrap/vulpes.cfg b/host/bootstrap/vulpes.cfg
index ebd6c8d1..460005ce 100644
--- a/host/bootstrap/vulpes.cfg
+++ b/host/bootstrap/vulpes.cfg
@@ -1,5 +1,9 @@
// Local Vulpes bootstrap, loaded only for the isolated host launcher.
try {
+ if (Services.env.get('VULPES_DEVICE') === 'sargo') {
+ // Keep a pinch gesture from scaling the phone UI as a whole.
+ pref('apz.allow_zooming', false);
+ }
if (Services.env.get('VULPES_HOST_ROOT')) {
var root = Components.classes['@mozilla.org/file/local;1'].createInstance(Components.interfaces.nsIFile);
root.initWithPath(Services.env.get('VULPES_HOST_ROOT'));
diff --git a/host/gaia-camera.js b/host/gaia-camera.js
index abd15a43..53d8e19f 100644
--- a/host/gaia-camera.js
+++ b/host/gaia-camera.js
@@ -1,7 +1,18 @@
// 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;
+ const supportedMime = candidates => typeof MediaRecorder !== 'undefined' &&
+ typeof MediaRecorder.isTypeSupported === 'function' ?
+ candidates.find(type => MediaRecorder.isTypeSupported(type)) || null : null;
+ const videoOnlyMime = supportedMime([
+ 'video/webm;codecs=vp8', 'video/webm;codecs=vp9', 'video/webm'
+ ]);
+ const videoAudioMime = supportedMime([
+ 'video/webm;codecs=vp8,opus', 'video/webm;codecs=vp9,opus'
+ ]);
+ const nativeCaptureAvailable = window.VulpesPlatform !== 'tundra' ||
+ typeof HTMLCanvasElement.prototype.captureStream === 'function';
+ const videoMime = nativeCaptureAvailable ? videoOnlyMime : null;
window.VulpesVideoExtension = videoMime ? 'webm' : null;
let cameras = ['back'],
devices = new Map(),
@@ -143,7 +154,7 @@
}
async function discover(stream, name) {
if (stream.vulpesNative) {
- cameras = ['back', 'front'];
+ cameras = stream.vulpesCameraMetadata.cameras || ['back', 'front'];
return;
}
// Device enumeration may wait indefinitely for focus in embedded desktop contexts.
@@ -178,19 +189,26 @@
const call = (params) => VulpesCompat.call('platform.cameraPreview', {...params, session});
const meta = await call({action:'start', camera:name === 'front' ? 'front' : 'back'});
session = meta.session;
+ const frameRate = meta.frameRate || 30;
const canvas = nativeCanvas || (nativeCanvas = document.createElement('canvas'));
const sideways = meta.orientation % 180 !== 0;
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.
+ // Prefer the GPU converter, but older feature-phone runtimes may expose no
+ // WebGL context at all. Their 320x240 preview is small enough for 2D canvas.
const gl = canvas.getContext('webgl', {alpha:false, antialias:false, preserveDrawingBuffer:true});
- if (!gl) {
+ const context2d = gl ? null : canvas.getContext('2d', {alpha:false});
+ if (!gl && !context2d) {
await call({action:'stop'});
- throw new DOMException('Camera preview requires WebGL', 'NotSupportedError');
+ throw new DOMException('Camera preview has no supported canvas renderer', 'NotSupportedError');
}
+ const rawCanvas = context2d ? document.createElement('canvas') : null;
+ if (rawCanvas) { rawCanvas.width=meta.width; rawCanvas.height=meta.height; }
+ const rawContext = rawCanvas?.getContext('2d', {alpha:false});
+ const imageData = rawContext?.createImageData(meta.width,meta.height);
+ const pixels = meta.width * meta.height;
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 = [];
@@ -200,12 +218,14 @@
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);
- // The next lens reuses this context after its old stream has stopped.
- gl.clearColor(0,0,0,1); gl.clear(gl.COLOR_BUFFER_BIT);
+ if (gl) {
+ textures.forEach(texture => gl.deleteTexture(texture));
+ shaders.forEach(value => gl.deleteShader(value));
+ if (buffer) gl.deleteBuffer(buffer);
+ if (program) gl.deleteProgram(program);
+ // The next lens reuses this context after its old stream has stopped.
+ gl.clearColor(0,0,0,1); gl.clear(gl.COLOR_BUFFER_BIT);
+ } else context2d.clearRect(0,0,canvas.width,canvas.height);
});
return closing;
};
@@ -217,6 +237,7 @@
if (!gl.getShaderParameter(value, gl.COMPILE_STATUS)) throw Error('Camera shader compilation failed');
return value;
}
+ if (gl) {
program = gl.createProgram();
gl.attachShader(program, shader(gl.VERTEX_SHADER, `
attribute vec2 position; varying vec2 coord;
@@ -253,11 +274,11 @@
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'});
@@ -267,11 +288,32 @@
if (data.length !== w * h * 3 / 2) throw Error('Invalid native camera frame');
sequence = frame.sequence; timing.sensorFrames = sequence;
const start=performance.now();
- gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,textures[0]);
- gl.texSubImage2D(gl.TEXTURE_2D,0,0,0,w,h,gl.LUMINANCE,gl.UNSIGNED_BYTE,data.subarray(0,pixels));
- gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D,textures[1]);
- gl.texSubImage2D(gl.TEXTURE_2D,0,0,0,w/2,h/2,gl.LUMINANCE_ALPHA,gl.UNSIGNED_BYTE,data.subarray(pixels));
- gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
+ if (gl) {
+ gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,textures[0]);
+ gl.texSubImage2D(gl.TEXTURE_2D,0,0,0,w,h,gl.LUMINANCE,gl.UNSIGNED_BYTE,data.subarray(0,pixels));
+ gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D,textures[1]);
+ gl.texSubImage2D(gl.TEXTURE_2D,0,0,0,w/2,h/2,gl.LUMINANCE_ALPHA,gl.UNSIGNED_BYTE,data.subarray(pixels));
+ gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
+ } else {
+ const rgba=imageData.data;
+ for(let y=0;y>1)*w;
+ for(let x=0;x>8));
+ rgba[offset+1]=Math.max(0,Math.min(255,(c-100*u-208*v+128)>>8));
+ rgba[offset+2]=Math.max(0,Math.min(255,(c+516*u+128)>>8));
+ rgba[offset+3]=255;
+ }
+ }
+ rawContext.putImageData(imageData,0,0);
+ context2d.save();
+ if(meta.orientation===90){context2d.translate(canvas.width,0);context2d.rotate(Math.PI/2);}
+ else if(meta.orientation===180){context2d.translate(canvas.width,canvas.height);context2d.rotate(Math.PI);}
+ else if(meta.orientation===270){context2d.translate(0,canvas.height);context2d.rotate(-Math.PI/2);}
+ context2d.drawImage(rawCanvas,0,0);context2d.restore();
+ }
timing.frames++;timing.conversionMs+=performance.now()-start;
return true;
};
@@ -302,9 +344,9 @@
if (!stopped) { recordError(error); await stop().catch(recordError); }
return;
}
- if (!stopped) timer = setTimeout(next, Math.max(0, 1000/30-(performance.now()-started)));
+ if (!stopped) timer = setTimeout(next, Math.max(0, 1000/frameRate-(performance.now()-started)));
};
- timer = setTimeout(next, 1000/30);
+ timer = setTimeout(next, 1000/frameRate);
return stream;
} catch (error) { await stop().catch(() => {}); throw error; }
}
@@ -408,7 +450,8 @@
let pictureSize = pictureSizes.reduce((a,b)=>a.width*a.height>b.width*b.height?a:b);
let flashMode = 'off', flashUpdate = Promise.resolve();
let focusArea = null, zoom = 1, mode = config.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 frameRate = native?.frameRate || 30;
+ const profile = {containerFormat:'webm',video:{width:size.width,height:size.height,codec:videoMime?.includes('vp9')?'vp9':'vp8',bitrate:frameRate < 10 ? 500000 : 1500000,framerate:frameRate},audio:{codec:'opus',samplerate:48000,channels:1,bitrate:64000}};
const configuration = () => ({
mode,
previewSize: size,
@@ -500,16 +543,55 @@
if(!videoMime)throw new DOMException('Video unavailable','NotSupportedError');
if(recording || startingRecording)throw new DOMException('Recording in progress','InvalidStateError');
startingRecording=true;
- let audio, captured;
+ let audio = null, captured, copyTimer, recordingCanvas, recordingContext;
try {
- audio=await navigator.mediaDevices.getUserMedia({audio:true,video:false});
+ // Video capture can still be useful when audio capture is unavailable.
+ // Camera track failures remain fatal; only the optional audio track falls back.
+ try {
+ audio=await navigator.mediaDevices.getUserMedia({audio:true,video:false});
+ } catch(error) {
+ // Tundra may expose the camera through its HAL while Gecko has no
+ // microphone capture backend. Audio is optional: that must not
+ // prevent the video encoder from starting.
+ console.info('Vulpes Camera: microphone unavailable; recording video without audio', error.name || error);
+ }
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;
+ if(native) {
+ // Gecko 156 on the Flip does not emit MediaRecorder data from a
+ // WebGL captureStream. Copy the already-rendered sensor preview
+ // into a small 2D canvas at the device's bounded frame rate.
+ const source=stream.vulpesPreviewCanvas;
+ recordingCanvas=document.createElement('canvas');
+ recordingCanvas.width=source.width;recordingCanvas.height=source.height;
+ recordingContext=recordingCanvas.getContext('2d',{alpha:false});
+ if(!recordingContext)throw new DOMException('Video canvas unavailable','NotSupportedError');
+ captured=recordingCanvas.captureStream(frameRate);
+ } else captured=new MediaStream(stream.getVideoTracks().map(t=>t.clone()));
+ for(const track of audio?.getAudioTracks() || [])captured.addTrack(track);
+ const recorderMime=audio && videoAudioMime ? videoAudioMime : videoOnlyMime;
+ if (audio && !videoAudioMime) {
+ audio.getTracks().forEach(track => track.stop());
+ audio = null;
+ }
+ const recorderOptions={mimeType:recorderMime,
+ videoBitsPerSecond:profile.video.bitrate,
+ ...(audio ? {audioBitsPerSecond:profile.audio.bitrate} : {})};
+ let recorder;
+ try {
+ recorder=new MediaRecorder(captured,recorderOptions);
+ } catch(error) {
+ if(!audio || !videoAudioMime)throw error;
+ // Some encoders report Opus support but fail to initialize it
+ // for a live device stream. Retry video-only instead of losing
+ // the entire recording.
+ audio.getTracks().forEach(track=>track.stop());audio=null;
+ captured.getAudioTracks().forEach(track=>captured.removeTrack(track));
+ recorder=new MediaRecorder(captured,{mimeType:videoOnlyMime,
+ videoBitsPerSecond:profile.video.bitrate});
+ }
+ const chunks=[];let bytes=0,failed=false,finish,sizeLimitTimer;
const done=new Promise(resolve=>{finish=resolve;});
const cap=Math.min(options.maxFileSizeBytes || Infinity,32*1024*1024);
recorder.ondataavailable=event=>{
@@ -521,9 +603,11 @@
};
recorder.onerror=()=>{failed=true;emit(stream,'recorderstatechange',{newState:'MediaRecorderFailed'});};
recorder.onstop=async()=>{
+ clearTimeout(sizeLimitTimer);
try {
if(failed || !bytes)throw Error('Empty or failed recording');
- const blob=new Blob(chunks,{type:'video/webm'});
+ const blob=new Blob(chunks,{type:recorder.mimeType || recorderMime || 'video/webm'});
+ if(blob.size>cap)throw new Error('Video exceeds the available storage limit');
await storage.addNamed(blob,filepath);
const canvas=document.createElement('canvas');
const source=native ? stream.vulpesPreviewCanvas : preview;
@@ -536,16 +620,44 @@
} catch(error) {
recordError(error);emit(stream,'recorderstatechange',{newState:'MediaRecorderFailed'});
} finally {
+ clearTimeout(copyTimer);
captured.getTracks().forEach(t=>t.stop());
+ recordingCanvas?.remove();
chunks.length=0;recording=null;finish();
emit(stream,'recorderstatechange',{newState:'Stopped'});
}
};
recording={recorder,done};
- recorder.start(500);
+ // Gecko 156's ARM/feature-phone media recorder can enter the
+ // inactive state without firing dataavailable/onstop when a
+ // timeslice is supplied. Collect the final chunk on stop instead.
+ recorder.start();
+ if(recordingCanvas) {
+ const track=captured.getVideoTracks()[0];
+ const copyFrame=()=>{
+ if(recorder.state!=='recording')return;
+ try {recordingContext.drawImage(stream.vulpesPreviewCanvas,0,0,recordingCanvas.width,recordingCanvas.height);track.requestFrame?.();}
+ catch(error) {recordError(error);}
+ if(recorder.state==='recording')copyTimer=setTimeout(copyFrame,Math.max(40,1000/frameRate));
+ };
+ recordingContext.drawImage(stream.vulpesPreviewCanvas,0,0,recordingCanvas.width,recordingCanvas.height);
+ track.requestFrame?.();
+ copyTimer=setTimeout(copyFrame,Math.max(40,1000/frameRate));
+ }
+ if(Number.isFinite(cap)) {
+ const bitrate=Math.max(1,profile.video.bitrate||500000);
+ sizeLimitTimer=setTimeout(()=>{
+ if(recorder.state==='recording') {
+ emit(stream,'recorderstatechange',{newState:'FileSizeLimitReached'});
+ recorder.stop();
+ }
+ },Math.max(1000,Math.floor(cap*8/bitrate*1000*.95)));
+ }
emit(stream,'recorderstatechange',{newState:'Started'});
} catch(error) {
+ clearTimeout(copyTimer);
captured?.getTracks().forEach(t=>t.stop());audio?.getTracks().forEach(t=>t.stop());
+ recordingCanvas?.remove();
recording=null;throw error;
} finally {startingRecording=false;}
},
diff --git a/host/gaia-compat.js b/host/gaia-compat.js
index c4d33c42..efb2fedc 100644
--- a/host/gaia-compat.js
+++ b/host/gaia-compat.js
@@ -30,9 +30,14 @@
const next = state.battery;
if (next && Number.isFinite(next.level) && next.level >= 0 && next.level <= 1) {
const oldLevel = battery.level, oldCharging = battery.charging;
+ const oldChargingTime = battery.chargingTime, oldDischargingTime = battery.dischargingTime;
battery.level = next.level; battery.charging = !!next.charging;
+ battery.chargingTime = Number.isFinite(next.chargingTime) && next.chargingTime > 0 ? next.chargingTime : Infinity;
+ battery.dischargingTime = Number.isFinite(next.dischargingTime) && next.dischargingTime > 0 ? next.dischargingTime : Infinity;
if (oldLevel !== battery.level) battery.dispatchEvent(new Event('levelchange'));
if (oldCharging !== battery.charging) battery.dispatchEvent(new Event('chargingchange'));
+ if (oldChargingTime !== battery.chargingTime) battery.dispatchEvent(new Event('chargingtimechange'));
+ if (oldDischargingTime !== battery.dischargingTime) battery.dispatchEvent(new Event('dischargingtimechange'));
}
} catch (_) { /* An unknown battery is not an empty battery. */ }
}
@@ -278,6 +283,12 @@
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);
+ if (event.detail?.type==='take-screenshot')
+ call('system.screenshot',{}).then(file=>dispatchEvent(new CustomEvent('mozChromeEvent',{
+ detail:{type:'take-screenshot-success',file}
+ }))).catch(error=>dispatchEvent(new CustomEvent('mozChromeEvent',{
+ detail:{type:'take-screenshot-error',error:error.message || 'SCREENSHOT_FAILED'}
+ })));
});
function application(record) {
// DOMApplication never exposed the registry's internal id. Gaia uses `id`
@@ -412,6 +423,12 @@
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 };
+ // Cost Control calls window.close() after its SIM-ready error dialog.
+ // Desktop and Android use Gaia's in-app task switcher, so close the app
+ // through the privileged host bridge instead of leaving the dialog stuck.
+ if (location.hostname === 'costcontrol.localhost') {
+ window.close = () => call('apps.close', {}).catch(console.error);
+ }
window.MozActivity = function (source) {
return request(call('activities.start', source));
};
diff --git a/host/gaia-theme.css b/host/gaia-theme.css
index 3e3ce004..093e1900 100644
--- a/host/gaia-theme.css
+++ b/host/gaia-theme.css
@@ -43,6 +43,18 @@
padding: 1rem 1.5rem;
transform: none;
}
+#display #vulpes-layout-row {
+ display: block !important;
+ min-height: 10rem;
+ height: auto !important;
+ padding-bottom: 1rem;
+}
+#vulpes-layout-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;
}
@@ -155,6 +167,12 @@ html[data-vulpes-layout="compact"][data-vulpes-app="homescreen"] #apps-panel > .
padding-top: 2px;
padding-bottom: 0;
}
+html[data-vulpes-app="homescreen"] gaia-grid#icons {
+ box-sizing: border-box;
+}
+html[data-vulpes-app="homescreen"] body.edit-mode gaia-grid#icons {
+ padding-bottom: calc(5rem + var(--software-home-button-height, 5rem));
+}
/* A single applications panel; saved pinned pages remain available in classic mode. */
html[data-vulpes-layout="compact"][data-vulpes-app="homescreen"] :is(#page-indicator-header, #pages-panel, #shadow) {
diff --git a/host/gaia-wifi.js b/host/gaia-wifi.js
index 1bde814d..6fbcabf9 100644
--- a/host/gaia-wifi.js
+++ b/host/gaia-wifi.js
@@ -191,7 +191,7 @@
}
// Settings follows delayed power changes. Adapters with live association
// state also refresh System, so reconnects update the status-bar icon.
- if(location.hostname.startsWith('settings.') || state.state) {
+ if(location.hostname.startsWith('settings.') || location.hostname.startsWith('system.')) {
let pending=false;
const refresh=setInterval(async()=>{
if((location.hostname.startsWith('settings.') && document.hidden) || pending) return;
diff --git a/overrides/camera.gaiamobile.org/js/main.js b/overrides/camera.gaiamobile.org/js/main.js
index 376b3c17..ad3374d3 100644
--- a/overrides/camera.gaiamobile.org/js/main.js
+++ b/overrides/camera.gaiamobile.org/js/main.js
@@ -2649,812 +2649,1496 @@ define('lib/mixin', [
return a;
};
});
-define('lib/camera/camera', [
- 'require',
- 'exports',
- 'module',
- 'lib/camera-utils',
- 'lib/orientation',
- 'lib/camera/focus',
- 'debug',
- 'lib/debounce',
- 'lib/bind-all',
- 'lib/mixin',
- 'model'
-], function (require, exports, module) {
- 'use strict';
- var CameraUtils = require('lib/camera-utils');
- var orientation = require('lib/orientation');
- var Focus = require('lib/camera/focus');
- var debug = require('debug')('camera');
- var debounce = require('lib/debounce');
- var bindAll = require('lib/bind-all');
- var mix = require('lib/mixin');
- var model = require('model');
- model(Camera.prototype);
- module.exports = Camera;
- function Camera(options) {
- debug('initializing');
- bindAll(this);
- options = options || {};
- this.cacheConfig = options.cacheConfig !== false;
- this.minRecordingTime = options.minRecordingTime || 1500;
- this.recordSpacePadding = options.recordSpacePadding || 1024 * 1024 * 1;
- this.recordSpaceMin = options.recordSpaceMin || 1024 * 1024 * 2;
- this.requestAttempts = options.requestAttempts || 3;
- this.orientation = options.orientation || orientation;
- this.configStorage = options.configStorage || localStorage;
- this.cameraList = navigator.mozCameras.getListOfCameras();
- this.mozCamera = null;
- this.storage = options.storage || {};
- this.video = {
- filepath: null,
- minSpace: this.recordSpaceMin,
- spacePadding: this.recordSpacePadding,
- poster: { filepath: null }
- };
- this.focus = new Focus(options.focus);
- this.suspendedFlashCount = 0;
- this.mode = 'picture';
- this.selectedCamera = 'back';
- this.configure = debounce(this.configure);
- debug('initialized');
+define("lib/camera/camera",["require", "exports", "module", "lib/camera-utils", "lib/orientation", "lib/camera/focus", "debug", "lib/debounce", "lib/bind-all", "lib/mixin", "model"],function(require, exports, module) {
+'use strict';
+
+/**
+ * Module Dependencies
+ */
+
+var CameraUtils = require('lib/camera-utils');
+var orientation = require('lib/orientation');
+var Focus = require('lib/camera/focus');
+var debug = require('debug')('camera');
+var debounce = require('lib/debounce');
+var bindAll = require('lib/bind-all');
+var mix = require('lib/mixin');
+var model = require('model');
+
+/**
+ * Mixin `Model` API (inc. events)
+ */
+
+model(Camera.prototype);
+
+/**
+ * Exports
+ */
+
+module.exports = Camera;
+
+/**
+ * Initialize a new 'Camera'
+ *
+ * Options:
+ *
+ * - {object} `focus`
+ *
+ * @param {Object} options
+ */
+function Camera(options) {
+ debug('initializing');
+ bindAll(this);
+
+ // Options
+ options = options || {};
+
+ // mozCamera config is cached by default
+ this.cacheConfig = options.cacheConfig !== false;
+
+ // Minimum video duration length for creating a
+ // video that contains at least few samples, see bug 899864.
+ // Empirical testing has shown that it is common to get corrupted
+ // files under 1400ms (e.g. on the nexus 5).
+ this.minRecordingTime = options.minRecordingTime || 1500;
+
+ // Number of bytes left on disk to let us stop recording.
+ this.recordSpacePadding = options.recordSpacePadding || 1024 * 1024 * 1;
+
+ // The minimum available disk space to start recording a video.
+ this.recordSpaceMin = options.recordSpaceMin || 1024 * 1024 * 2;
+
+ // The number of times to attempt
+ // hardware request before giving up
+ this.requestAttempts = options.requestAttempts || 3;
+
+ // Test hooks
+ this.orientation = options.orientation || orientation;
+ this.configStorage = options.configStorage || localStorage;
+
+ this.cameraList = navigator.mozCameras.getListOfCameras();
+ this.mozCamera = null;
+
+ this.storage = options.storage || {};
+
+ // Video config
+ this.video = {
+ filepath: null,
+ minSpace: this.recordSpaceMin,
+ spacePadding : this.recordSpacePadding,
+ poster: {
+ filepath: null,
}
- Camera.prototype.load = function () {
- debug('load camera');
- var loadingNewCamera = this.selectedCamera !== this.lastLoadedCamera;
- var self = this;
- if (this.releasing) {
- debug('wait for camera release');
- this.once('released', function () {
- self.load();
- });
- return;
- }
- if (this.mozCamera && !loadingNewCamera) {
- debug('camera not changed');
- this.ready();
- return;
- }
- if (this.mozCamera) {
- this.release(ready);
- } else {
- ready();
- }
- function ready() {
- self.requestCamera(self.selectedCamera);
- self.lastLoadedCamera = self.selectedCamera;
- }
+ };
+
+ this.focus = new Focus(options.focus);
+ this.suspendedFlashCount = 0;
+
+ // Always boot in 'picture' mode
+ // with 'back' camera. This may need
+ // to be configurable at some point.
+ this.mode = 'picture';
+ this.selectedCamera = 'back';
+
+ // Allow `configure` to be called multiple
+ // times in the same frame, but only ever run once.
+ this.configure = debounce(this.configure);
+
+ debug('initialized');
+}
+
+/**
+ * Loads the currently selected camera.
+ *
+ * There are cases whereby the camera
+ * may still be 'releasing' its hardware.
+ * If this is the case we wait for the
+ * release process to finish, then attempt
+ * to load again.
+ *
+ * @public
+ */
+Camera.prototype.load = function() {
+ debug('load camera');
+
+ var loadingNewCamera = this.selectedCamera !== this.lastLoadedCamera;
+ var self = this;
+
+ // If hardware is still being released
+ // we're not allowed to request the camera.
+ if (this.releasing) {
+ debug('wait for camera release');
+ this.once('released', function() { self.load(); });
+ return;
+ }
+
+ // Don't re-load hardware if selected camera is the same.
+ if (this.mozCamera && !loadingNewCamera) {
+ debug('camera not changed');
+ this.ready();
+ return;
+ }
+
+ // If a camera is already loaded,
+ // it must be 'released' first.
+ // We also discard the `mozCameraConfig`
+ // as the previous camera config
+ // won't apply to the new camera.
+ if (this.mozCamera) {
+ this.release(ready);
+ } else {
+ ready();
+ }
+
+ // Once ready we request the camera
+ // with the currently `selectedCamera`
+ // and any `mozCameraConfig` that may
+ // be in memory.
+ //
+ // The only time there should be a
+ // valid `mozCameraConfig` in memory
+ // is when the app becomes visible again
+ // after being hidden. and we wish to
+ // request the camera again in exactly
+ // the same state it was previously in.
+ function ready() {
+ self.requestCamera(self.selectedCamera);
+ self.lastLoadedCamera = self.selectedCamera;
+ }
+};
+
+/**
+ * Requests the mozCamera object,
+ * then configures it.
+ *
+ * @private
+ */
+Camera.prototype.requestCamera = function(camera, config) {
+ if (!config) {
+ config = {
+ mode: this.mode
};
- Camera.prototype.requestCamera = function (camera, config) {
- if (!config) {
- config = { mode: this.mode };
- }
- debug('request camera', camera, config);
- if (this.isBusy) {
- return;
- }
- var attempts = this.requestAttempts;
- var self = this;
- this.configured = false;
- this.busy('requestingCamera');
- request();
- function request() {
- navigator.mozCameras.getCamera(camera, config).then(onSuccess, onError);
- self.emit('requesting');
- debug('camera requested', camera, config);
- attempts--;
- }
- function onSuccess(result) {
- debug('successfully got mozCamera', result);
- if (document.hidden) {
- self.mozCamera = result.camera;
- self.release();
- return;
- }
- self.updateConfig(result.configuration);
- self.setupNewCamera(result.camera);
- self.configureFocus();
- self.emit('focusconfigured', {
- mode: self.mozCamera.focusMode,
- touchFocus: self.focus.touchFocus,
- faceDetection: self.focus.faceDetection,
- maxDetectedFaces: self.focus.maxDetectedFaces
- });
- if (self.configured) {
- self.emit('configured');
- self.onPreviewStateChange({ newState: 'started' });
- }
- self.ready();
- }
- function onError(err) {
- debug('error requesting camera', err);
- if (err.name === 'NS_ERROR_NOT_INITIALIZED' && attempts) {
- self.cameraRequestTimeout = setTimeout(request, 1000);
- return;
- }
- self.emit('error', 'request-fail');
- self.ready();
- }
- };
- Camera.prototype.updateConfig = function (config) {
- this.givenPreviewSize = config.previewSize;
- this.pictureSize = config.pictureSize;
- this.recorderProfile = config.recorderProfile;
- this.configured = true;
- };
- Camera.prototype.setupNewCamera = function (mozCamera) {
- debug('configuring camera');
- var capabilities = mozCamera.capabilities;
- this.mozCamera = mozCamera;
- this.mozCamera.addEventListener('shutter', this.onShutter);
- this.mozCamera.addEventListener('close', this.onClosed);
- this.mozCamera.addEventListener('previewstatechange', this.onPreviewStateChange);
- this.mozCamera.addEventListener('recorderstatechange', this.onRecorderStateChange);
- this.mozCamera.addEventListener('poster', this.onPoster);
- this.capabilities = this.formatCapabilities(capabilities);
- this.emit('newcamera', {
- capabilities: this.capabilities,
- pictureSize: this.pictureSize,
- recorderProfile: this.recorderProfile
- });
- debug('configured new camera');
- };
- Camera.prototype.formatCapabilities = function (capabilities) {
- var hasHDR = capabilities.sceneModes.indexOf('hdr') > -1;
- var hdr = hasHDR ? [
- 'on',
- 'off'
- ] : undefined;
- return mix({ hdr: hdr }, capabilities);
- };
- Camera.prototype.configure = function () {
- debug('configuring hardware...');
- var self = this;
- this.configured = false;
- if (this.isBusy) {
- debug('defering configuration');
- this.once('ready', this.configure);
- return;
- }
- if (!this.mozCamera) {
- debug('no mozCamera');
- return;
- }
- this.stopRecording();
- this.busy();
- var mozCameraConfig = {
- mode: this.mode,
- pictureSize: this.pictureSize,
- recorderProfile: this.recorderProfile
- };
- this.mozCamera.setConfiguration(mozCameraConfig).then(onSuccess, onError);
- debug('mozCamera configuring', mozCameraConfig);
- function onSuccess(config) {
- debug('configuration success');
- if (!self.mozCamera) {
- return;
- }
- self.updateConfig(config);
- self.configureFocus();
- self.emit('configured');
- self.ready();
- }
- function onError(err) {
- debug('Error configuring camera');
- self.configured = true;
- self.ready();
- }
- };
- Camera.prototype.configureFocus = function () {
- this.focus.configure(this.mozCamera, this.mode);
- this.focus.onFacesDetected = this.onFacesDetected;
- this.focus.onAutoFocusChanged = this.onAutoFocusChanged;
- };
- Camera.prototype.shutdown = function () {
- this.stopRecording();
- this.set('previewActive', false);
- this.set('focus', 'none');
- this.release();
- };
- Camera.prototype.onAutoFocusChanged = function (state) {
- this.set('focus', state);
- };
- Camera.prototype.onFacesDetected = function (faces) {
- this.emit('facesdetected', faces);
- };
- Camera.prototype.loadStreamInto = function (videoElement) {
- debug('loading stream into element');
- if (!this.mozCamera) {
- debug('error - `mozCamera` is undefined or null');
- return;
- }
- if (!videoElement) {
- debug('error - `videoElement` is undefined or null');
- return;
- }
- var isCurrent = videoElement.mozSrcObject === this.mozCamera;
- if (isCurrent) {
- return debug('camera didn\'t change');
- }
- videoElement.mozSrcObject = this.mozCamera;
- videoElement.play();
- debug('stream loaded into video');
- };
- Camera.prototype.previewSizes = function () {
- if (!this.mozCamera) {
- return;
- }
- return this.mozCamera.capabilities.previewSizes;
- };
- Camera.prototype.previewSize = function () {
- return this.givenPreviewSize;
- };
- Camera.prototype.resolution = function () {
- switch (this.mode) {
- case 'picture':
- return this.pictureSize;
- case 'video':
- return this.getRecorderProfile().video;
- }
- };
- Camera.prototype.setPictureSize = function (size, options) {
- debug('set picture size', size);
- if (!size) {
- return;
- }
- var configure = !(options && options.configure === false);
- if (this.isPictureSize(size)) {
- debug('pictureSize didn\'t change');
- return;
- }
- this.pictureSize = size;
- if (configure) {
- this.configure();
- } else {
- this.mozCamera.setPictureSize(size);
- }
- this.setThumbnailSize();
- debug('pictureSize changed');
- return this;
- };
- Camera.prototype.isPictureSize = function (size) {
- if (!this.pictureSize) {
- return false;
- }
- var sameWidth = size.width === this.pictureSize.width;
- var sameHeight = size.height === this.pictureSize.height;
- return sameWidth && sameHeight;
- };
- Camera.prototype.setRecorderProfile = function (key, options) {
- debug('set recorderProfile: %s', key);
- if (!key) {
- return;
- }
- var configure = !(options && options.configure === false);
- if (this.isRecorderProfile(key)) {
- debug('recorderProfile didn\'t change');
- return;
- }
- this.recorderProfile = key;
- if (configure) {
- this.configure();
- }
- debug('recorderProfile changed: %s', key);
- return this;
- };
- Camera.prototype.isRecorderProfile = function (key) {
- return key === this.recorderProfile;
- };
- Camera.prototype.getRecorderProfile = function () {
- var key = this.recorderProfile;
- return this.mozCamera.capabilities.recorderProfiles[key];
- };
- Camera.prototype.setThumbnailSize = function () {
- var sizes = this.mozCamera.capabilities.thumbnailSizes;
- var pictureSize = this.mozCamera.getPictureSize();
- var picked = this.pickThumbnailSize(sizes, pictureSize);
- if (picked) {
- this.mozCamera.setThumbnailSize(picked);
- }
- };
- Camera.prototype.setFlashMode = function (key) {
- if (this.mozCamera) {
- key = key || 'off';
- if (this.suspendedFlashCount > 0) {
- this.suspendedFlashMode = key;
- debug('flash mode set while suspended: %s', key);
- } else {
- this.mozCamera.flashMode = key;
- debug('flash mode set: %s', key);
- }
- }
- return this;
- };
- Camera.prototype.release = function (done) {
- debug('release');
- done = done || function () {
- };
- var self = this;
- clearTimeout(this.cameraRequestTimeout);
- clearTimeout(this.zoomChangeTimeout);
- this.zoomChangeTimeout = null;
- if (!this.mozCamera) {
- done();
- return;
- }
- this.busy();
- this.stopRecording();
- this.set('focus', 'none');
- this.mozCamera.release().then(onSuccess, onError);
- this.releasing = true;
- this.mozCamera = null;
- delete this.pictureSize;
- delete this.recorderProfile;
- delete this.givenPreviewSize;
- function onSuccess() {
- debug('successfully released');
- self.ready();
- self.releasing = false;
- self.emit('released');
- done();
- }
- function onError(err) {
- debug('failed to release hardware');
- self.ready();
- self.releasing = false;
- done(err);
- }
- };
- Camera.prototype.pickThumbnailSize = function (thumbnailSizes, pictureSize) {
- var screenWidth = window.innerWidth * window.devicePixelRatio;
- var screenHeight = window.innerHeight * window.devicePixelRatio;
- var pictureAspectRatio = pictureSize.width / pictureSize.height;
- var currentThumbnailSize;
- var i;
- thumbnailSizes = thumbnailSizes.slice(0);
- if (!thumbnailSizes || !pictureSize) {
- return;
- }
- function imageSizeFillsScreen(pixelsWidth, pixelsHeight) {
- return (pixelsWidth >= screenWidth || pixelsHeight >= screenHeight) && (pixelsWidth >= screenHeight || pixelsHeight >= screenWidth);
- }
- thumbnailSizes = thumbnailSizes.filter(function (thumbnailSize) {
- var thumbnailAspectRatio = thumbnailSize.width / thumbnailSize.height;
- return Math.abs(thumbnailAspectRatio - pictureAspectRatio) < 0.05;
- });
- if (thumbnailSizes.length === 0) {
- console.error('Error while selecting thumbnail size. ' + 'There are no thumbnail sizes that match the ratio of ' + 'the selected picture size: ' + JSON.stringify(pictureSize));
- return;
- }
- thumbnailSizes.sort(function (a, b) {
- return a.width * a.height - b.width * b.height;
- });
- for (i = 0; i < thumbnailSizes.length; ++i) {
- currentThumbnailSize = thumbnailSizes[i];
- if (imageSizeFillsScreen(currentThumbnailSize.width, currentThumbnailSize.height)) {
- return currentThumbnailSize;
- }
- }
- return thumbnailSizes[thumbnailSizes.length - 1];
- };
- Camera.prototype.capture = function (options) {
- if (!this.mozCamera) {
- return false;
- }
- switch (this.mode) {
- case 'picture':
- this.takePicture(options);
- break;
- case 'video':
- this.toggleRecording(options);
- break;
- }
- };
- Camera.prototype.takePicture = function (options) {
- debug('take picture', options);
- this.busy();
- var rotation = this.orientation.get();
- var selectedCamera = this.selectedCamera;
- var self = this;
- var position = options && options.position;
- var config = {
- dateTime: Date.now() / 1000,
- pictureSize: self.pictureSize,
- fileFormat: 'jpeg'
- };
- if (position) {
- config.position = position;
- }
- config.rotation = selectedCamera === 'front' ? -rotation : rotation;
- if (this.focus.getMode() === 'auto') {
- this.focus.focus(onFocused);
- } else {
- takePicture();
- }
- function onFocused(state) {
- takePicture();
- }
- function takePicture() {
- self.busy('takingPicture');
- self.mozCamera.takePicture(config).then(onSuccess, onError);
- }
- function onError(error) {
- if (error.name === 'NS_ERROR_IN_PROGRESS') {
- complete();
- } else {
- document.l10n.formatValue('error-saving').then(value => {
- alert(value);
- complete();
- });
- }
- }
- function onSuccess(blob) {
- var image = { blob: blob };
- self.resumePreview();
- self.set('focus', 'none');
- self.emit('newimage', image);
- debug('success taking picture');
- complete();
- }
- function complete() {
- self.set('focus', 'none');
- self.ready();
- }
- };
- Camera.prototype.updateFocusArea = function (rect, done) {
- this.focus.updateFocusArea(rect, focusDone);
- function focusDone(state) {
- if (done) {
- done(state);
- }
- }
- };
- Camera.prototype.toggleRecording = function (options) {
- var state = this.get('recording');
- if (state && state !== 'stopped') {
- this.stopRecording();
- } else {
- this.startRecording(options);
- }
- };
- Camera.prototype.setStorage = function (storage) {
- this.storage.video = storage.video;
- };
- Camera.prototype.startRecording = function (options) {
- debug('start recording');
- var frontCamera = this.selectedCamera === 'front';
- var rotation = this.orientation.get();
- var storage = this.storage.video;
- var video = this.video;
- var self = this;
- if (frontCamera) {
- rotation = -rotation;
- }
- this.set('recording', 'starting');
- this.busy();
- this.orientation.stop();
- var profile = this.resolution();
- video.width = profile.width;
- video.height = profile.height;
- video.rotation = this.orientation.rationalize(this.getSensorAngle(), rotation);
- var previewSize = this.previewSize();
- video.poster.width = previewSize.width;
- video.poster.height = previewSize.height;
- video.poster.rotation = video.rotation;
- this.getFreeVideoStorageSpace(gotStorageSpace);
- function gotStorageSpace(err, freeBytes) {
- if (self.get('recording') === 'stopping') {
- debug('start recording interrupted (getFreeVideoStorageSpace)');
- return self.stoppedRecording();
- }
- if (err) {
- return self.onStartRecordingError();
- }
- var notEnoughSpace = freeBytes < self.video.minSpace;
- var remaining = freeBytes - self.video.spacePadding;
- var targetFileSize = self.get('maxFileSizeBytes');
- var maxFileSizeBytes = targetFileSize || remaining;
- if (notEnoughSpace) {
- self.onStartRecordingError('nospace2');
- return;
- }
- var config = {
- rotation: rotation,
- maxFileSizeBytes: maxFileSizeBytes,
- createPoster: true
- };
- self.createVideoFilepath(createVideoFilepathDone);
- function createVideoFilepathDone(errorMsg, filepath) {
- if (self.get('recording') === 'stopping') {
- debug('start recording interrupted (createVideoFilepath)');
- return self.stoppedRecording();
- }
- if (typeof filepath === 'undefined') {
- debug(errorMsg);
- return self.onStartRecordingError('error-video-file-path');
- }
- video.filepath = filepath;
- self.emit('willrecord');
- self.mozCamera.startRecording(config, storage, filepath).then(onSuccess, onError);
- }
- }
- function onError(err) {
- storage.delete(video.filepath);
- if (err.name === 'NS_ERROR_IN_PROGRESS') {
- debug('start recording error (in progress)');
- return;
- }
- if (err.name === 'NS_ERROR_ABORT') {
- debug('start recording error (abort)');
- return self.stoppedRecording();
- }
- self.onStartRecordingError();
- }
- function onSuccess() {
- self.ready();
- if (document.hidden) {
- self.stopRecording();
- }
- }
- };
- Camera.prototype.startedRecording = function () {
- debug('started recording');
- this.startVideoTimer();
- this.set('recording', 'started');
- };
- Camera.prototype.stopRecording = function () {
- debug('stop recording');
- var state = this.get('recording');
- if (!state || state === 'stopping' || state === 'stopped' || state === 'error') {
- debug('not recording or stop pending');
- return;
- }
- this.set('recording', 'stopping');
- this.busy();
- this.mozCamera.stopRecording();
- };
- Camera.prototype.stoppedRecording = function (recorded) {
- debug('stopped recording');
- this.stopVideoTimer();
- if (!recorded) {
- this.set('recording', 'error');
- }
- this.set('recording', 'stopped');
- this.orientation.start();
- var self = this;
- var video;
- if (recorded) {
- video = mix({}, this.video);
- video.poster = mix({}, video.poster);
- this.storage.video.get(video.filepath).then(function (blob) {
- video.blob = blob;
- self.emit('newvideo', video);
- self.ready();
- }, function () {
- if (self) {
- self.onStopRecordingError(video);
- self = null;
- }
- });
- }
- };
- Camera.prototype.onRecordingError = function (id) {
- if (id) {
- document.l10n.formatValue(id).then(value => {
- alert(value);
- });
- }
- this.ready();
- };
- Camera.prototype.onStartRecordingError = function (id) {
- debug('start record error');
- this.stoppedRecording();
- this.onRecordingError(id);
- };
- Camera.prototype.onStopRecordingError = function (video) {
- debug('stop record error');
- this.storage.video.delete(video.filepath);
- var elapsedTime = this.get('videoElapsed');
- if (elapsedTime < this.minRecordingTime) {
- this.ready();
- } else {
- this.onRecordingError();
- }
- };
- Camera.prototype.onShutter = function () {
- this.emit('shutter');
- };
- Camera.prototype.onClosed = function (e) {
- this.shutdown();
- this.emit('closed', e.reason);
- };
- Camera.prototype.onPreviewStateChange = function (e) {
- var state = e.newState;
- debug('preview state change: %s', state);
- if (state === this.previewState) {
- return;
- }
- this.previewState = state;
- this.emit('preview:' + state);
- };
- Camera.prototype.onRecorderStateChange = function (e) {
- var msg = e.newState;
- debug('recorder state change: %s', msg);
- if (msg === 'FileSizeLimitReached') {
- this.emit('filesizelimitreached');
- } else if (msg === 'Started') {
- this.startedRecording();
- } else if (msg === 'Stopped') {
- if (this.stopRecordError) {
- this.onStopRecordingError(this.video);
- }
- this.stoppedRecording(!this.stopRecordError);
- this.stopRecordError = false;
- } else if (msg === 'PosterFailed' || msg === 'TrackFailed' || msg === 'MediaRecorderFailed' || msg === 'MediaServerFailed') {
- this.stopRecordError = true;
- this.stopRecording();
- }
- };
- Camera.prototype.onPoster = function (e) {
- debug('poster created');
- this.video.poster.blob = e.data;
- };
- Camera.prototype.getFreeVideoStorageSpace = function (done) {
- debug('get free storage space');
- var storage = this.storage.video;
- var req = storage.freeSpace();
- req.onerror = onError;
- req.onsuccess = onSuccess;
- function onSuccess() {
- var freeBytes = req.result;
- debug('%d free space found', freeBytes);
- done(null, freeBytes);
- }
- function onError() {
- done('error');
- }
- };
- Camera.prototype.createVideoFilepath = function (done) {
- done(null, Date.now() + '_tmp.3gp');
- };
- Camera.prototype.resumePreview = function () {
- this.mozCamera.resumePreview();
- this.focus.startFaceDetection();
- this.emit('previewresumed');
- };
- Camera.prototype.setCamera = function (camera) {
- debug('set camera: %s', camera);
- if (this.selectedCamera === camera) {
- return;
- }
- this.selectedCamera = camera;
- this.load();
- };
- Camera.prototype.setMode = function (mode) {
- debug('setting mode to: %s', mode);
- if (this.isMode(mode)) {
- return;
- }
- this.mode = mode;
- this.configure();
- return this;
- };
- Camera.prototype.isMode = function (mode) {
- return this.mode === mode;
- };
- Camera.prototype.startVideoTimer = function () {
- this.set('videoStart', new Date().getTime());
- this.videoTimer = setInterval(this.updateVideoElapsed, 1000);
- this.updateVideoElapsed();
- };
- Camera.prototype.stopVideoTimer = function () {
- clearInterval(this.videoTimer);
- this.videoTimer = null;
- this.updateVideoElapsed();
- };
- Camera.prototype.updateVideoElapsed = function () {
- var now = new Date().getTime();
- var start = this.get('videoStart');
- this.set('videoElapsed', now - start);
- };
- Camera.prototype.setISOMode = function (value) {
- var isoModes = this.mozCamera.capabilities.isoModes;
- if (isoModes && isoModes.indexOf(value) > -1) {
- this.mozCamera.isoMode = value;
- }
- };
- Camera.prototype.setWhiteBalance = function (value) {
- var capabilities = this.mozCamera.capabilities;
- var modes = capabilities.whiteBalanceModes;
- if (modes && modes.indexOf(value) > -1) {
- this.mozCamera.whiteBalanceMode = value;
- }
- };
- Camera.prototype.setHDR = function (value) {
- debug('set hdr: %s', value);
- if (!value) {
- return;
- }
- var scene = value === 'on' ? 'hdr' : 'auto';
- this.setSceneMode(scene);
- };
- Camera.prototype.setSceneMode = function (value) {
- var modes = this.mozCamera.capabilities.sceneModes;
- if (modes.indexOf(value) > -1) {
- this.mozCamera.sceneMode = value;
- }
- };
- Camera.prototype.isZoomSupported = function () {
- if (!this.mozCamera) {
- return false;
- }
- return this.mozCamera.capabilities.zoomRatios.length > 1;
- };
- Camera.prototype.configureZoom = function () {
- var previewSize = this.previewSize();
- var maxPreviewSize = CameraUtils.getMaximumPreviewSize(this.previewSizes());
- var maxHardwareZoom = maxPreviewSize.width / previewSize.width;
- this.set('maxHardwareZoom', maxHardwareZoom);
- this.setZoom(this.getMinimumZoom());
- this.emit('zoomconfigured', this.getZoom());
- return this;
- };
- Camera.prototype.getMinimumZoom = function () {
- var zoomRatios = this.mozCamera.capabilities.zoomRatios;
- if (zoomRatios.length === 0) {
- return 1;
- }
- return zoomRatios[0];
- };
- Camera.prototype.getMaximumZoom = function () {
- var zoomRatios = this.mozCamera.capabilities.zoomRatios;
- if (zoomRatios.length === 0) {
- return 1;
- }
- return zoomRatios[zoomRatios.length - 1];
- };
- Camera.prototype.getZoom = function () {
- return this.mozCamera.zoom;
- };
- Camera.prototype.setZoom = function (zoom) {
- this.zoom = zoom;
- this.emit('zoomchanged', this.zoom);
- if (this.zoomChangeTimeout) {
- return;
- }
- var self = this;
- this.zoomChangeTimeout = window.setTimeout(function () {
- self.zoomChangeTimeout = null;
- self.mozCamera.zoom = self.zoom;
- }, 150);
- };
- Camera.prototype.getZoomPreviewAdjustment = function () {
- var zoom = this.mozCamera.zoom;
- var maxHardwareZoom = this.get('maxHardwareZoom');
- if (zoom <= maxHardwareZoom) {
- return 1;
- }
- return zoom / maxHardwareZoom;
- };
- Camera.prototype.getSensorAngle = function () {
- return this.mozCamera && this.mozCamera.sensorAngle;
- };
- Camera.prototype.busy = function (type) {
- debug('busy %s', type || '');
- this.isBusy = true;
- this.emit('busy', type);
- clearTimeout(this.readyTimeout);
- };
- Camera.prototype.ready = function () {
- var self = this;
- this.isBusy = false;
- clearTimeout(this.readyTimeout);
- this.readyTimeout = setTimeout(function () {
- debug('ready');
- self.emit('ready');
- }, 150);
+ }
+
+ debug('request camera', camera, config);
+ if (this.isBusy) { return; }
+
+ var attempts = this.requestAttempts;
+ var self = this;
+
+ // Indicate 'busy'
+ this.configured = false;
+ this.busy('requestingCamera');
+
+ // Make initial request
+ request();
+
+ /**
+ * Requests the camera hardware.
+ *
+ * @private
+ */
+ function request() {
+ navigator.mozCameras.getCamera(camera, config)
+ .then(onSuccess, onError);
+
+ self.emit('requesting');
+ debug('camera requested', camera, config);
+ attempts--;
+ }
+
+ function onSuccess(result) {
+ debug('successfully got mozCamera', result);
+
+ // release camera when press power key
+ // as soon as you open a camera app
+ if (document.hidden) {
+ self.mozCamera = result.camera;
+ self.release();
+ return;
+ }
+
+ self.updateConfig(result.configuration);
+ self.setupNewCamera(result.camera);
+ self.configureFocus();
+ self.emit('focusconfigured', {
+ mode: self.mozCamera.focusMode,
+ touchFocus: self.focus.touchFocus,
+ faceDetection: self.focus.faceDetection,
+ maxDetectedFaces: self.focus.maxDetectedFaces
+ });
+
+ // If the camera was configured in the
+ // `mozCamera.getCamera()` call, we can
+ // fire the 'configured' event now. We
+ // can also start the preview because
+ // gecko ensures that configuration only
+ // completes after the preview has been
+ // started. While we will get a preview
+ // started event after this, we can often
+ // save ourselves getting delayed by an
+ // extra refresh by configuring the
+ // viewfinder now.
+ if (self.configured) {
+ self.emit('configured');
+ self.onPreviewStateChange({newState: 'started'});
+ }
+
+ self.ready();
+ }
+
+ /**
+ * Called when the request for camera
+ * hardware fails.
+ *
+ * If the hardware is 'closed' we attempt
+ * to re-request it one second later, until
+ * all our attempts have run out.
+ *
+ * @param {String} err
+ */
+ function onError(err) {
+ debug('error requesting camera', err);
+
+ if (err.name === 'NS_ERROR_NOT_INITIALIZED' && attempts) {
+ self.cameraRequestTimeout = setTimeout(request, 1000);
+ return;
+ }
+
+ self.emit('error', 'request-fail');
+ self.ready();
+ }
+};
+
+Camera.prototype.updateConfig = function(config) {
+ this.givenPreviewSize = config.previewSize;
+ this.pictureSize = config.pictureSize;
+ this.recorderProfile = config.recorderProfile;
+ this.configured = true;
+};
+
+/**
+ * Configures the newly recieved
+ * mozCamera object.
+ *
+ * Setting the 'cababilities' key
+ * triggers 'change' callback inside
+ * the CameraController that sets the
+ * app up for the new camera.
+ *
+ * @param {MozCamera} mozCamera
+ * @private
+ */
+Camera.prototype.setupNewCamera = function(mozCamera) {
+ debug('configuring camera');
+ var capabilities = mozCamera.capabilities;
+ this.mozCamera = mozCamera;
+
+ // Bind to some events
+ this.mozCamera.addEventListener('shutter', this.onShutter);
+ this.mozCamera.addEventListener('close', this.onClosed);
+ this.mozCamera.addEventListener('previewstatechange',
+ this.onPreviewStateChange);
+ this.mozCamera.addEventListener('recorderstatechange',
+ this.onRecorderStateChange);
+ this.mozCamera.addEventListener('poster', this.onPoster);
+
+ this.capabilities = this.formatCapabilities(capabilities);
+
+ this.emit('newcamera', {
+ capabilities: this.capabilities,
+ pictureSize: this.pictureSize,
+ recorderProfile: this.recorderProfile
+ });
+
+ debug('configured new camera');
+};
+
+/**
+ * Camera capablities need to be in
+ * a consistent format.
+ *
+ * We shallow clone to make sure the
+ * app doesnt' make changes to the
+ * original `capabilities` object.
+ *
+ * @param {Object} capabilities
+ * @return {Object}
+ */
+Camera.prototype.formatCapabilities = function(capabilities) {
+ var hasHDR = capabilities.sceneModes.indexOf('hdr') > -1;
+ var hdr = hasHDR ? ['on', 'off'] : undefined;
+ return mix({ hdr: hdr }, capabilities);
+};
+
+/**
+ * Configure the camera hardware
+ * with the current `mode`, `previewSize`
+ * and `recorderProfile`.
+ *
+ * @private
+ */
+Camera.prototype.configure = function() {
+ debug('configuring hardware...');
+ var self = this;
+
+ // As soon as a request to configure
+ // comes in, the confuguration is now
+ // dirty (out-of-date), and the hardware
+ // must be reconfigured at some point.
+ this.configured = false;
+
+ // Ensure that any requests that
+ // come in whilst busy get run once
+ // camera is ready again.
+ if (this.isBusy) {
+ debug('defering configuration');
+ this.once('ready', this.configure);
+ return;
+ }
+
+ // Exit here if there is no camera
+ if (!this.mozCamera) {
+ debug('no mozCamera');
+ return;
+ }
+
+ // In some extreme cases the mode can
+ // get changed and configured whilst
+ // video recording is in progress.
+ this.stopRecording();
+
+ // Indicate 'busy'
+ this.busy();
+
+ // Create a new `mozCameraConfig`
+ var mozCameraConfig = {
+ mode: this.mode,
+ pictureSize: this.pictureSize,
+ recorderProfile: this.recorderProfile
+ };
+
+ // Configure the camera hardware
+ this.mozCamera.setConfiguration(mozCameraConfig)
+ .then(onSuccess, onError);
+ debug('mozCamera configuring', mozCameraConfig);
+
+ function onSuccess(config) {
+ debug('configuration success');
+ if (!self.mozCamera) { return; }
+ self.updateConfig(config);
+ self.configureFocus();
+ self.emit('configured');
+ self.ready();
+ }
+
+ function onError(err) {
+ debug('Error configuring camera');
+ self.configured = true;
+ self.ready();
+ }
+};
+
+Camera.prototype.configureFocus = function() {
+ this.focus.configure(this.mozCamera, this.mode);
+ this.focus.onFacesDetected = this.onFacesDetected;
+ this.focus.onAutoFocusChanged = this.onAutoFocusChanged;
+};
+
+Camera.prototype.shutdown = function() {
+ this.stopRecording();
+ this.set('previewActive', false);
+ this.set('focus', 'none');
+ this.release();
+};
+
+Camera.prototype.onAutoFocusChanged = function(state) {
+ this.set('focus', state);
+};
+
+Camera.prototype.onFacesDetected = function(faces) {
+ this.emit('facesdetected', faces);
+};
+
+/**
+ * Plugs Video Stream into Video Element.
+ *
+ * @param {Elmement} videoElement
+ * @public
+ */
+Camera.prototype.loadStreamInto = function(videoElement) {
+ debug('loading stream into element');
+ if (!this.mozCamera) {
+ debug('error - `mozCamera` is undefined or null');
+ return;
+ }
+
+ // REVIEW: Something is wrong if we are
+ // calling this without a video element.
+ if (!videoElement) {
+ debug('error - `videoElement` is undefined or null');
+ return;
+ }
+
+ // Don't load the same camera stream again
+ var isCurrent = videoElement.mozSrcObject === this.mozCamera;
+ if (isCurrent) { return debug('camera didn\'t change'); }
+
+ videoElement.mozSrcObject = this.mozCamera;
+ videoElement.play();
+ debug('stream loaded into video');
+};
+
+/**
+ * Return available preview sizes.
+ *
+ * @return {Array}
+ * @private
+ */
+Camera.prototype.previewSizes = function() {
+ if (!this.mozCamera) { return; }
+ return this.mozCamera.capabilities.previewSizes;
+};
+
+/**
+ * Return the current optimal preview size.
+ *
+ * @return {Object}
+ * @private
+ */
+Camera.prototype.previewSize = function() {
+ return this.givenPreviewSize;
+};
+
+/**
+ * Get the current recording resolution.
+ *
+ * @return {Object}
+ */
+Camera.prototype.resolution = function() {
+ switch (this.mode) {
+ case 'picture': return this.pictureSize;
+ case 'video': return this.getRecorderProfile().video;
+ }
+};
+
+/**
+ * Set the picture size.
+ *
+ * If the given size is the same as the
+ * currently set pictureSize then no
+ * action is taken.
+ *
+ * The camera is 'configured' a soon as the
+ * pictureSize is changed. `.configure` is
+ * debounced so it will only ever run once
+ * per turn.
+ *
+ * Options:
+ *
+ * - {Boolean} `configure`
+ *
+ * @param {Object} size
+ */
+Camera.prototype.setPictureSize = function(size, options) {
+ debug('set picture size', size);
+ if (!size) { return; }
+
+ // Configure unless `false`
+ var configure = !(options && options.configure === false);
+
+ // Don't do waste time re-configuring the
+ // hardware if the pictureSize hasn't changed.
+ if (this.isPictureSize(size)) {
+ debug('pictureSize didn\'t change');
+ return;
+ }
+
+ this.pictureSize = size;
+
+ // Configure the hardware only when required
+ if (configure) {
+ this.configure();
+ } else {
+ this.mozCamera.setPictureSize(size);
+ }
+ this.setThumbnailSize();
+
+ debug('pictureSize changed');
+ return this;
+};
+
+Camera.prototype.isPictureSize = function(size) {
+ if (!this.pictureSize) { return false; }
+ var sameWidth = size.width === this.pictureSize.width;
+ var sameHeight = size.height === this.pictureSize.height;
+ return sameWidth && sameHeight;
+};
+
+/**
+ * Set the recorder profile.
+ *
+ * If the given profile is the same as
+ * the current profile, no action is
+ * taken.
+ *
+ * The camera is 'configured' a soon as the
+ * recorderProfile is changed (`.configure()` is
+ * debounced so it will only ever run once
+ * per turn).
+ *
+ * Options:
+ *
+ * - {Boolean} `configure`
+ *
+ * @param {String} key
+ */
+Camera.prototype.setRecorderProfile = function(key, options) {
+ debug('set recorderProfile: %s', key);
+ if (!key) { return; }
+
+ // Configure unless `false`
+ var configure = !(options && options.configure === false);
+
+ // Exit if not changed
+ if (this.isRecorderProfile(key)) {
+ debug('recorderProfile didn\'t change');
+ return;
+ }
+
+ this.recorderProfile = key;
+ if (configure) { this.configure(); }
+
+ debug('recorderProfile changed: %s', key);
+ return this;
+};
+
+Camera.prototype.isRecorderProfile = function(key) {
+ return key === this.recorderProfile;
+};
+
+/**
+ * Returns the full profile of the
+ * currently set recordrProfile.
+ *
+ * @return {Object}
+ */
+Camera.prototype.getRecorderProfile = function() {
+ var key = this.recorderProfile;
+ return this.mozCamera.capabilities.recorderProfiles[key];
+};
+
+Camera.prototype.setThumbnailSize = function() {
+ var sizes = this.mozCamera.capabilities.thumbnailSizes;
+ var pictureSize = this.mozCamera.getPictureSize();
+ var picked = this.pickThumbnailSize(sizes, pictureSize);
+ if (picked) { this.mozCamera.setThumbnailSize(picked); }
+};
+
+/**
+ * Sets the current flash mode,
+ * both on the Camera instance
+ * and on the cameraObj hardware.
+ * If flash is suspended, it
+ * updates the cached state that
+ * will be restored.
+ *
+ * @param {String} key
+ */
+Camera.prototype.setFlashMode = function(key) {
+ if (this.mozCamera) {
+ // If no key was provided, set it to 'off' which is
+ // a valid flash mode.
+ key = key || 'off';
+
+ if (this.suspendedFlashCount > 0) {
+ this.suspendedFlashMode = key;
+ debug('flash mode set while suspended: %s', key);
+ } else {
+ this.mozCamera.flashMode = key;
+ debug('flash mode set: %s', key);
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Releases the camera hardware.
+ *
+ * @param {Function} done
+ */
+Camera.prototype.release = function(done) {
+ debug('release');
+ done = done || function() {};
+ var self = this;
+
+ // Clear any pending hardware requests
+ clearTimeout(this.cameraRequestTimeout);
+ clearTimeout(this.zoomChangeTimeout);
+ this.zoomChangeTimeout = null;
+
+ // Ignore if there is no loaded camera
+ if (!this.mozCamera) {
+ done();
+ return;
+ }
+
+ this.busy();
+ this.stopRecording();
+ this.set('focus', 'none');
+ this.mozCamera.release().then(onSuccess, onError);
+ this.releasing = true;
+ this.mozCamera = null;
+
+ // Reset cached parameters
+ delete this.pictureSize;
+ delete this.recorderProfile;
+ delete this.givenPreviewSize;
+
+ function onSuccess() {
+ debug('successfully released');
+ self.ready();
+ self.releasing = false;
+ self.emit('released');
+ done();
+ }
+
+ function onError(err) {
+ debug('failed to release hardware');
+ self.ready();
+ self.releasing = false;
+ done(err);
+ }
+};
+
+// TODO: Perhaps this function should be moved into a separate lib
+Camera.prototype.pickThumbnailSize = function(thumbnailSizes, pictureSize) {
+ var screenWidth = window.innerWidth * window.devicePixelRatio;
+ var screenHeight = window.innerHeight * window.devicePixelRatio;
+ var pictureAspectRatio = pictureSize.width / pictureSize.height;
+ var currentThumbnailSize;
+ var i;
+
+ // Coping the array to not modify the original
+ thumbnailSizes = thumbnailSizes.slice(0);
+ if (!thumbnailSizes || !pictureSize) {
+ return;
+ }
+
+ function imageSizeFillsScreen(pixelsWidth, pixelsHeight) {
+ return ((pixelsWidth >= screenWidth || // portrait
+ pixelsHeight >= screenHeight) &&
+ (pixelsWidth >= screenHeight || // landscape
+ pixelsHeight >= screenWidth));
+ }
+
+ // Removes the sizes with the wrong aspect ratio
+ thumbnailSizes = thumbnailSizes.filter(function(thumbnailSize) {
+ var thumbnailAspectRatio = thumbnailSize.width / thumbnailSize.height;
+ return Math.abs(thumbnailAspectRatio - pictureAspectRatio) < 0.05;
+ });
+
+ if (thumbnailSizes.length === 0) {
+ console.error('Error while selecting thumbnail size. ' +
+ 'There are no thumbnail sizes that match the ratio of ' +
+ 'the selected picture size: ' + JSON.stringify(pictureSize));
+ return;
+ }
+
+ // Sorting the array from smaller to larger sizes
+ thumbnailSizes.sort(function(a, b) {
+ return a.width * a.height - b.width * b.height;
+ });
+
+ for (i = 0; i < thumbnailSizes.length; ++i) {
+ currentThumbnailSize = thumbnailSizes[i];
+ if (imageSizeFillsScreen(currentThumbnailSize.width,
+ currentThumbnailSize.height)) {
+ return currentThumbnailSize;
+ }
+ }
+
+ return thumbnailSizes[thumbnailSizes.length - 1];
+};
+
+/**
+ * Takes a photo, or begins/ends
+ * a video capture session.
+ *
+ * Options:
+ *
+ * - `position` {Object} - geolocation to store in EXIF
+ *
+ * @param {Object} options
+ * @public
+ */
+Camera.prototype.capture = function(options) {
+ if (!this.mozCamera) { return false; }
+ switch (this.mode) {
+ case 'picture': this.takePicture(options); break;
+ case 'video': this.toggleRecording(options); break;
+ }
+};
+
+/**
+ * Take a picture.
+ *
+ * Options:
+ *
+ * - {Number} `position` - geolocation to store in EXIF
+ *
+ * @param {Object} options
+ */
+Camera.prototype.takePicture = function(options) {
+ debug('take picture', options);
+ this.busy();
+
+ var rotation = this.orientation.get();
+ var selectedCamera = this.selectedCamera;
+ var self = this;
+ var position = options && options.position;
+ var config = {
+ dateTime: Date.now() / 1000,
+ pictureSize: self.pictureSize,
+ fileFormat: 'jpeg'
+ };
+
+ // If position has been passed in,
+ // add it to the config object.
+ if (position) {
+ config.position = position;
+ }
+
+ // Front camera is inverted, so flip rotation
+ config.rotation = selectedCamera === 'front' ? -rotation : rotation;
+
+ // If the camera focus is 'continuous' or 'infinity'
+ // we can take the picture straight away.
+ if (this.focus.getMode() === 'auto') {
+ this.focus.focus(onFocused);
+ } else {
+ takePicture();
+ }
+
+ function onFocused(state) {
+ takePicture();
+ }
+
+ function takePicture() {
+ self.busy('takingPicture');
+ self.mozCamera.takePicture(config).then(onSuccess, onError);
+ }
+
+ function onError(error) {
+ // if taking a picture fails because there's
+ // already a picture being taken we ignore it.
+ if (error.name === 'NS_ERROR_IN_PROGRESS') {
+ complete();
+ } else {
+ document.l10n.formatValue('error-saving').then((value) => {
+ alert(value);
+ complete();
+ });
+ }
+ }
+
+ function onSuccess(blob) {
+ var image = { blob: blob };
+ self.resumePreview();
+ self.set('focus', 'none');
+ self.emit('newimage', image);
+ debug('success taking picture');
+ complete();
+ }
+
+ function complete() {
+ self.set('focus', 'none');
+ self.ready();
+ }
+};
+
+Camera.prototype.updateFocusArea = function(rect, done) {
+ this.focus.updateFocusArea(rect, focusDone);
+ function focusDone(state) {
+ if (done) {
+ done(state);
+ }
+ }
+};
+
+/**
+ * Start/stop recording.
+ *
+ * @param {Object} options
+ */
+Camera.prototype.toggleRecording = function(options) {
+ var state = this.get('recording');
+ if (state && state !== 'stopped') { this.stopRecording(); }
+ else { this.startRecording(options); }
+};
+
+/**
+ * Seet the storage for video.
+ *
+ * @public
+ */
+Camera.prototype.setStorage = function(storage) {
+ this.storage.video = storage.video;
+};
+
+/**
+ * Start recording a video.
+ *
+ * @public
+ */
+Camera.prototype.startRecording = function(options) {
+ debug('start recording');
+ var frontCamera = this.selectedCamera === 'front';
+ var rotation = this.orientation.get();
+ var storage = this.storage.video;
+ var video = this.video;
+ var self = this;
+
+ // Rotation is flipped for front camera
+ if (frontCamera) { rotation = -rotation; }
+
+ this.set('recording', 'starting');
+ this.busy();
+
+ // Lock orientation during video recording
+ //
+ // REVIEW: This should *not* be here. This
+ // is an App concern and should live in
+ // the `CameraController`.
+ this.orientation.stop();
+
+ var profile = this.resolution();
+ video.width = profile.width;
+ video.height = profile.height;
+
+ // Calculate what the configured rotation of the video will be
+ // so that we don't have to load the metadata to find this out
+ video.rotation = this.orientation.rationalize(this.getSensorAngle(),
+ rotation);
+
+ var previewSize = this.previewSize();
+ video.poster.width = previewSize.width;
+ video.poster.height = previewSize.height;
+ video.poster.rotation = video.rotation;
+
+ // First check if there is enough free space
+ this.getFreeVideoStorageSpace(gotStorageSpace);
+
+ function gotStorageSpace(err, freeBytes) {
+ if (self.get('recording') === 'stopping') {
+ debug('start recording interrupted (getFreeVideoStorageSpace)');
+ return self.stoppedRecording();
+ }
+
+ if (err) { return self.onStartRecordingError(); }
+
+ var notEnoughSpace = freeBytes < self.video.minSpace;
+ var remaining = freeBytes - self.video.spacePadding;
+ var targetFileSize = self.get('maxFileSizeBytes');
+ var maxFileSizeBytes = targetFileSize || remaining;
+
+ // Don't continue if there
+ // is not enough space
+ if (notEnoughSpace) {
+ self.onStartRecordingError('nospace2');
+ return;
+ }
+
+ // TODO: Callee should
+ // pass in orientation
+ var config = {
+ rotation: rotation,
+ maxFileSizeBytes: maxFileSizeBytes,
+ createPoster: true
};
+
+ self.createVideoFilepath(createVideoFilepathDone);
+
+ function createVideoFilepathDone(errorMsg, filepath) {
+ if (self.get('recording') === 'stopping') {
+ debug('start recording interrupted (createVideoFilepath)');
+ return self.stoppedRecording();
+ }
+
+ if (typeof filepath === 'undefined') {
+ debug(errorMsg);
+ return self.onStartRecordingError('error-video-file-path');
+ }
+
+ video.filepath = filepath;
+ self.emit('willrecord');
+ self.mozCamera.startRecording(config, storage, filepath)
+ .then(onSuccess, onError);
+ }
+ }
+
+ function onError(err) {
+ // Regardless of error, if we called out to mozCamera, it may have
+ // created an empty file and we need to clean that up.
+ storage.delete(video.filepath);
+ // IN_PROGRESS means there is already a startRecording call in
+ // progress; we can ignore it because it will have its own duplicated
+ // success/error path.
+ if (err.name === 'NS_ERROR_IN_PROGRESS') {
+ debug('start recording error (in progress)');
+ return;
+ }
+ // ABORT means a stopRecording call interrupted us. Since it never
+ // transitioned to the Started recording state, we need to stop
+ // explicitly here.
+ if (err.name === 'NS_ERROR_ABORT') {
+ debug('start recording error (abort)');
+ return self.stoppedRecording();
+ }
+ // Ignore err as we use our own set of error
+ // codes; instead trigger using the default
+ self.onStartRecordingError();
+ }
+
+ function onSuccess() {
+ self.ready();
+
+ // User closed app while
+ // recording was trying to start
+ //
+ // TODO: Not sure this should be here
+ if (document.hidden) {
+ self.stopRecording();
+ }
+ }
+};
+
+Camera.prototype.startedRecording = function() {
+ debug('started recording');
+ this.startVideoTimer();
+ this.set('recording', 'started');
+};
+
+/**
+ * Stop recording the video.
+ *
+ * Once we have told the camera to stop recording
+ * the video we attach a 'change' listener to the
+ * video storage and wait. Once the listener fires
+ * we know that the video file has been saved.
+ *
+ * At this point we fetch the file Blob from
+ * storage and then call the `.onNewVideo()`
+ * method to handle the final stages.
+ *
+ * @public
+ */
+Camera.prototype.stopRecording = function() {
+ debug('stop recording');
+
+ var state = this.get('recording');
+
+ // Even if we have requested a recording to stop, that doesn't
+ // mean it has finished yet, as we need to wait for the recorder
+ // state change event.
+ if (!state || state === 'stopping' || state === 'stopped' ||
+ state === 'error') {
+ debug('not recording or stop pending');
+ return;
+ }
+
+ this.set('recording', 'stopping');
+ this.busy();
+ this.mozCamera.stopRecording();
+};
+
+Camera.prototype.stoppedRecording = function(recorded) {
+ debug('stopped recording');
+ this.stopVideoTimer();
+ if (!recorded) {
+ this.set('recording', 'error');
+ }
+ this.set('recording', 'stopped');
+
+ // Unlock orientation when stopping video recording.
+ // REVIEW:WP This logic is out of scope of the
+ // Camera hardware. Only the App should be
+ // making such high level decicions.
+ this.orientation.start();
+
+ var self = this;
+ var video;
+
+ if (recorded) {
+ video = mix({}, this.video);
+ video.poster = mix({}, video.poster);
+
+ // Re-fetch the blobs from storage
+ this.storage.video.get(video.filepath).then(function(blob) {
+ video.blob = blob;
+ // Tell the app the new video is ready
+ self.emit('newvideo', video);
+ self.ready();
+ }, function() {
+ if (self) {
+ self.onStopRecordingError(video);
+ self = null;
+ }
+ });
+ }
+};
+
+// TODO: This is UI stuff, so
+// shouldn't be handled in this file.
+Camera.prototype.onRecordingError = function(id) {
+ if (id) {
+ document.l10n.formatValue(id).then((value) => {
+ alert(value);
+ });
+ }
+ this.ready();
+};
+
+Camera.prototype.onStartRecordingError = function(id) {
+ debug('start record error');
+ this.stoppedRecording();
+ this.onRecordingError(id);
+};
+
+Camera.prototype.onStopRecordingError = function(video) {
+ debug('stop record error');
+
+ // This file may or may not exist, delete it just in case
+ this.storage.video.delete(video.filepath);
+
+ // If the time between start/stop was really short, suppress the
+ // error dialog to the user -- they wouldn't have expected to
+ // get a recording anyways
+ var elapsedTime = this.get('videoElapsed');
+ if (elapsedTime < this.minRecordingTime) {
+ this.ready();
+ } else {
+ this.onRecordingError();
+ }
+};
+
+/**
+ * Emit a 'shutter' event so that
+ * app UI can respond with shutter
+ * animations and sounds effects.
+ *
+ * @private
+ */
+Camera.prototype.onShutter = function() {
+ this.emit('shutter');
+};
+
+/**
+ * Emit a 'closed' event when camera controller
+ * closes
+ *
+ * @private
+ */
+Camera.prototype.onClosed = function(e) {
+ this.shutdown();
+ this.emit('closed', e.reason);
+};
+
+/**
+ * The preview state change events come
+ * from the camera hardware. If 'stopped'
+ * or 'paused' the camera must not be used.
+ *
+ * @param event with {String} newState ['started', 'stopped', 'paused']
+ * @private
+ */
+Camera.prototype.onPreviewStateChange = function(e) {
+ var state = e.newState;
+ debug('preview state change: %s', state);
+ if (state === this.previewState) { return; }
+ this.previewState = state;
+ this.emit('preview:' + state);
+};
+
+/**
+ * Emit useful event hook.
+ *
+ * @param {String} msg
+ * @private
+ */
+Camera.prototype.onRecorderStateChange = function(e) {
+ var msg = e.newState;
+ debug('recorder state change: %s', msg);
+ if (msg === 'FileSizeLimitReached') {
+ this.emit('filesizelimitreached');
+ } else if(msg === 'Started') {
+ this.startedRecording();
+ } else if(msg === 'Stopped') {
+ // The last event to come in is always Stopped; if an asynchronous
+ // error happened (i.e. couldn't create the poster), we need to
+ // report the error to the user.
+ if (this.stopRecordError) {
+ this.onStopRecordingError(this.video);
+ }
+ this.stoppedRecording(!this.stopRecordError);
+ this.stopRecordError = false;
+ } else if(msg === 'PosterFailed' || msg === 'TrackFailed' ||
+ msg === 'MediaRecorderFailed' || msg === 'MediaServerFailed')
+ {
+ // TrackFailed is triggered when the video has no samples (i.e.
+ // is too short) and would fail to be loaded by the application
+ this.stopRecordError = true;
+ this.stopRecording();
+ }
+};
+
+/**
+ * Requested poster got created while recording a video.
+ *
+ * @private
+ */
+Camera.prototype.onPoster = function(e) {
+ debug('poster created');
+ this.video.poster.blob = e.data;
+};
+
+/**
+ * Get the number of remaining
+ * bytes in video storage.
+ *
+ * @param {Function} done
+ * @async
+ * @private
+ */
+Camera.prototype.getFreeVideoStorageSpace = function(done) {
+ debug('get free storage space');
+
+ var storage = this.storage.video;
+ var req = storage.freeSpace();
+ req.onerror = onError;
+ req.onsuccess = onSuccess;
+
+ function onSuccess() {
+ var freeBytes = req.result;
+ debug('%d free space found', freeBytes);
+ done(null, freeBytes);
+ }
+
+ function onError() {
+ done('error');
+ }
+};
+
+/**
+ * Get a unique video filepath
+ * to record a new video to.
+ *
+ * Your application can overwrite
+ * this method with something
+ * so that you can record directly
+ * to final location. We do this
+ * in CameraController.
+ *
+ * Callback function signature used
+ * so that an async override can
+ * be used if you wish.
+ *
+ * @param {Function} done
+ */
+Camera.prototype.createVideoFilepath = function(done) {
+ done(null, Date.now() + '_tmp.3gp');
+};
+
+/**
+ * Resume the preview stream.
+ *
+ * After a photo has been taken the
+ * preview stream freezes on the
+ * taken frame. We call this function
+ * to start the stream flowing again.
+ *
+ * @private
+ */
+Camera.prototype.resumePreview = function() {
+ this.mozCamera.resumePreview();
+ // After calling takePicture(Camera.ShutterCallback, Camera.PictureCallback,
+ // Camera.PictureCallback) or stopPreview(), and then resuming preview with
+ // startPreview(), the apps should call this method again to resume face
+ // detection. See Bug 1070791.
+ this.focus.startFaceDetection();
+ this.emit('previewresumed');
+};
+
+/**
+ * Sets the selected camera to the
+ * given string and then reloads
+ * the camera.
+ *
+ * If the given camera is already
+ * selected, no action is taken.
+ *
+ * @param {String} camera 'front'|'back'
+ * @public
+ */
+Camera.prototype.setCamera = function(camera) {
+ debug('set camera: %s', camera);
+ if (this.selectedCamera === camera) { return; }
+ this.selectedCamera = camera;
+ this.load();
+};
+
+/**
+ * Toggles between 'picture'
+ * and 'video' capture modes.
+ *
+ * @return {String}
+ * @public
+ */
+Camera.prototype.setMode = function(mode) {
+ debug('setting mode to: %s', mode);
+ if (this.isMode(mode)) { return; }
+ this.mode = mode;
+ this.configure();
+ return this;
+};
+
+/**
+ * States if the camera is currently
+ * set to the passed mode.
+ *
+ * @param {String} mode ['picture'|'video']
+ * @return {Boolean}
+ * @public
+ */
+Camera.prototype.isMode = function(mode) {
+ return this.mode === mode;
+};
+
+/**
+ * Sets a start time and begins
+ * updating the elapsed time
+ * every second.
+ *
+ * @private
+ */
+Camera.prototype.startVideoTimer = function() {
+ this.set('videoStart', new Date().getTime());
+ this.videoTimer = setInterval(this.updateVideoElapsed, 1000);
+ this.updateVideoElapsed();
+};
+
+/**
+ * Clear the video timer interval.
+ *
+ * @private
+ */
+Camera.prototype.stopVideoTimer = function() {
+ clearInterval(this.videoTimer);
+ this.videoTimer = null;
+ this.updateVideoElapsed();
+};
+
+/**
+ * Calculates the elapse time
+ * and updateds the 'videoElapsed'
+ * value.
+ *
+ * We listen for the 'change:'
+ * event emitted elsewhere to
+ * update the UI accordingly.
+ *
+ */
+Camera.prototype.updateVideoElapsed = function() {
+ var now = new Date().getTime();
+ var start = this.get('videoStart');
+ this.set('videoElapsed', (now - start));
+};
+
+/**
+ * Set ISO value.
+ *
+ * @param {String} value
+ * @public
+ */
+Camera.prototype.setISOMode = function(value) {
+ var isoModes = this.mozCamera.capabilities.isoModes;
+ if (isoModes && isoModes.indexOf(value) > -1) {
+ this.mozCamera.isoMode = value;
+ }
+};
+
+/**
+ * Set the mozCamera white-balance value.
+ *
+ * @param {String} value
+ * @public
+ */
+Camera.prototype.setWhiteBalance = function(value){
+ var capabilities = this.mozCamera.capabilities;
+ var modes = capabilities.whiteBalanceModes;
+ if (modes && modes.indexOf(value) > -1) {
+ this.mozCamera.whiteBalanceMode = value;
+ }
+};
+
+/**
+ * Set HDR mode.
+ *
+ * HDR is a scene mode, so we
+ * transform the hdr value to
+ * the appropriate scene value.
+ *
+ * @param {String} value
+ * @public
+ */
+Camera.prototype.setHDR = function(value){
+ debug('set hdr: %s', value);
+ if (!value) { return; }
+ var scene = value === 'on' ? 'hdr' : 'auto';
+ this.setSceneMode(scene);
+};
+
+/**
+ * Set scene mode.
+ *
+ * @param {String} value
+ * @public
+ */
+Camera.prototype.setSceneMode = function(value){
+ var modes = this.mozCamera.capabilities.sceneModes;
+ if (modes.indexOf(value) > -1) {
+ this.mozCamera.sceneMode = value;
+ }
+};
+
+/**
+ * Check if the hardware supports zoom.
+ *
+ * @return {Boolean}
+ */
+Camera.prototype.isZoomSupported = function() {
+ if (!this.mozCamera) {
+ return false;
+ }
+ return this.mozCamera.capabilities.zoomRatios.length > 1;
+};
+
+Camera.prototype.configureZoom = function() {
+ var previewSize = this.previewSize();
+ var maxPreviewSize =
+ CameraUtils.getMaximumPreviewSize(this.previewSizes());
+
+ // Calculate the maximum amount of zoom that the hardware will
+ // perform. This calculation is determined by taking the maximum
+ // supported preview size *width* and dividing by the current preview
+ // size *width*.
+ var maxHardwareZoom = maxPreviewSize.width / previewSize.width;
+ this.set('maxHardwareZoom', maxHardwareZoom);
+
+ this.setZoom(this.getMinimumZoom());
+ this.emit('zoomconfigured', this.getZoom());
+ return this;
+};
+
+Camera.prototype.getMinimumZoom = function() {
+ var zoomRatios = this.mozCamera.capabilities.zoomRatios;
+ if (zoomRatios.length === 0) {
+ return 1.0;
+ }
+
+ return zoomRatios[0];
+};
+
+Camera.prototype.getMaximumZoom = function() {
+ var zoomRatios = this.mozCamera.capabilities.zoomRatios;
+ if (zoomRatios.length === 0) {
+ return 1.0;
+ }
+
+ return zoomRatios[zoomRatios.length - 1];
+};
+
+Camera.prototype.getZoom = function() {
+ return this.mozCamera.zoom;
+};
+
+Camera.prototype.setZoom = function(zoom) {
+ this.zoom = zoom;
+ this.emit('zoomchanged', this.zoom);
+
+ // Stop here if we're already waiting for
+ // `mozCamera.zoom` to be updated.
+ if (this.zoomChangeTimeout) {
+ return;
+ }
+
+ var self = this;
+
+ // Throttle to prevent hammering the Camera API.
+ this.zoomChangeTimeout = window.setTimeout(function() {
+ self.zoomChangeTimeout = null;
+
+ self.mozCamera.zoom = self.zoom;
+ }, 150);
+};
+
+Camera.prototype.getZoomPreviewAdjustment = function() {
+ var zoom = this.mozCamera.zoom;
+ var maxHardwareZoom = this.get('maxHardwareZoom');
+ if (zoom <= maxHardwareZoom) {
+ return 1.0;
+ }
+
+ return zoom / maxHardwareZoom;
+};
+
+/**
+ * Retrieves the angle of orientation of the camera hardware's
+ * image sensor. This value is calculated as the angle that the
+ * camera image needs to be rotated (clockwise) so that it appears
+ * correctly on the display in the device's natural (portrait)
+ * orientation
+ *
+ * Reference:
+ * http://developer.android.com/reference/android/hardware/Camera.CameraInfo.html#orientation
+ *
+ * @return {Number}
+ * @public
+ */
+Camera.prototype.getSensorAngle = function() {
+ return this.mozCamera && this.mozCamera.sensorAngle;
+};
+
+/**
+ * A central place to indicate
+ * the camera is 'busy'.
+ *
+ * @private
+ */
+Camera.prototype.busy = function(type) {
+ debug('busy %s', type || '');
+ this.isBusy = true;
+ this.emit('busy', type);
+ clearTimeout(this.readyTimeout);
+};
+
+/**
+ * A central place to indicate
+ * the camera is 'ready'.
+ *
+ * @private
+ */
+Camera.prototype.ready = function() {
+ var self = this;
+ this.isBusy = false;
+ clearTimeout(this.readyTimeout);
+ this.readyTimeout = setTimeout(function() {
+ debug('ready');
+ self.emit('ready');
+ }, 150);
+};
+
});
(function (exports) {
'use strict';
@@ -3919,492 +4603,831 @@ define('lib/pinch', [
return newDistance - oldDistance;
}
});
-define('app', [
- 'require',
- 'exports',
- 'module',
- 'stop-recording-event',
- 'views/notification',
- 'views/loading-screen',
- 'lib/orientation',
- 'lib/bind-all',
- 'debug',
- 'lib/pinch',
- 'lib/bind',
- 'model'
-], function (require, exports, module) {
- 'use strict';
- var stopRecordingEvent = require('stop-recording-event');
- var NotificationView = require('views/notification');
- var LoadingView = require('views/loading-screen');
- var orientation = require('lib/orientation');
- var bindAll = require('lib/bind-all');
- var debug = require('debug')('app');
- var Pinch = require('lib/pinch');
- var bind = require('lib/bind');
- var model = require('model');
- module.exports = App;
- model(App.prototype);
- function App(options) {
- debug('initialize');
- bindAll(this);
- this.views = {};
- this.dynamicLazy = {};
- this.el = options.el;
- this.win = options.win;
- this.doc = options.doc;
- this.perf = options.perf || {};
- this.pinch = options.pinch || new Pinch(this.el);
- this.require = options.require || window.requirejs;
- this.LoadingView = options.LoadingView || LoadingView;
- this.orientation = options.orientation || orientation;
- this.inSecureMode = this.win.location.hash === '#secure';
- this.controllers = options.controllers;
- this.geolocation = options.geolocation;
- this.settings = options.settings;
- this.camera = options.camera;
- this.activity = {};
- this.sounds = options.sounds;
- debug('initialized');
- }
- App.prototype.boot = function () {
- debug('boot');
- if (this.booted) {
- return;
- }
- this.showSpinner('requestingCamera');
- this.bindEvents();
- this.initializeViews();
- this.runControllers();
- window.performance.mark('navigationLoaded');
- window.performance.mark('navigationInteractive');
- this.injectViews();
- this.booted = true;
- debug('booted');
- };
- App.prototype.dispatchEvent = function (name) {
- this.win.dispatchEvent(new CustomEvent(name));
- };
- App.prototype.runControllers = function () {
- debug('run controllers');
- this.controllers.overlay(this);
- this.controllers.battery(this);
- this.controllers.settings(this);
- this.controllers.activity(this);
- this.controllers.camera(this);
- this.controllers.viewfinder(this);
- this.controllers.hud(this);
- this.controllers.controls(this);
- this.controllers.storage(this);
- debug('controllers run');
- };
- App.prototype.loadLazyController = function (controller) {
- if (!this.criticalPathDone) {
- this.controllers.lazy.push(controller);
- return;
- }
- if (!this.dynamicLazy[controller]) {
- this.dynamicLazy[controller] = this.loadLazyControllers([controller]);
- }
- return this.dynamicLazy[controller];
- };
- App.prototype.loadLazyControllers = function (controllers) {
- debug('load lazy controllers');
- var self = this;
- return new Promise(function (resolve, reject) {
- self.require(controllers, function () {
- [].forEach.call(arguments, function (controller) {
- controller(self);
- });
- debug('controllers loaded');
- self.emit('lazyloaded');
- resolve();
- });
- });
- };
- App.prototype.initializeViews = function () {
- debug('initializing views');
- this.views.notification = new NotificationView();
- debug('views initialized');
- };
- App.prototype.injectViews = function () {
- debug('injecting views');
- this.views.notification.appendTo(this.el);
- debug('views injected');
- };
- App.prototype.bindEvents = function () {
- debug('binding events');
- this.once('storage:checked:healthy', this.geolocationWatch);
- this.on('storage:checked', function() { this.storageChecked = true; }.bind(this));
- this.once('viewfinder:visible', this.onCriticalPathDone);
- this.once('camera:error', this.onCriticalPathDone);
- this.once('activity', this.onActivity);
- this.once('newthumbnail', this.onNewThumbnail);
- this.once('preview', this.onPreview);
- this.once('camera:willchange', this.onWillChange);
- this.on('camera:willchange', this.firer('busy'));
- this.on('ready', this.clearSpinner);
- this.on('visible', this.onVisible);
- this.on('hidden', this.onHidden);
- this.on('reboot', this.onReboot);
- this.on('busy', this.onBusy);
- if (this.settings.countdown.selected('key') !== 'off') {
- this.onCountdown();
- } else {
- this.settings.countdown.once('change:selected', this.onCountdown);
- }
- this.pinch.on('changed', this.firer('pinch:changed'));
- this.on('previewgallery:opened', this.pinch.disable);
- this.on('previewgallery:closed', this.pinch.enable);
- this.on('settings:opened', this.pinch.disable);
- this.on('settings:closed', this.pinch.enable);
- bind(this.doc, 'visibilitychange', this.onVisibilityChange);
- bind(this.doc, 'DOMRetranslated', this.firer('localized'));
- bind(this.win, 'beforeunload', this.onBeforeUnload);
- bind(this.win, 'keydown', this.onKeyDown);
- bind(this.el, 'click', this.onClick);
- debug('events bound');
- };
- App.prototype.onVisible = function () {
- this.geolocationWatch();
- this.orientation.start();
- this.orientation.lock();
- debug('visible');
- };
- App.prototype.onHidden = function () {
- this.geolocation.stopWatching();
- this.orientation.stop();
- debug('hidden');
- };
- App.prototype.onReboot = function () {
- debug('reboot');
- window.location.reload();
- };
- App.prototype.onClick = function () {
- debug('click');
- this.emit('click');
- };
- App.prototype.onCriticalPathDone = function () {
- if (this.criticalPathDone) {
- return;
- }
- debug('critical path done');
- window.performance.mark('visuallyLoaded');
- this.listenForStopRecordingEvent();
- this.loadLazyModules();
- this.perf.criticalPath = Date.now();
- this.criticalPathDone = true;
- this.emit('criticalpathdone');
- };
- App.prototype.onActivity = function () {
- this.loadLazyController('controllers/confirm');
- };
- App.prototype.onCountdown = function () {
- this.loadLazyController('controllers/countdown');
- };
- App.prototype.onNewThumbnail = function () {
- this.loadLazyController('controllers/preview-gallery');
- };
- App.prototype.onPreview = function () {
- var self = this;
- this.emit('busy', 'lazyLoading');
- this.loadLazyController('controllers/preview-gallery').then(function () {
- self.emit('preview');
- self.emit('ready');
- });
- };
- App.prototype.onWillChange = function () {
- this.loadLazyController('controllers/recording-timer');
- };
- App.prototype.loadLazyModules = function () {
- debug('load lazy modules');
- var self = this;
- var load = this.loadLazyControllers(this.controllers.lazy);
- var storage = new Promise(function (resolve, reject) {
- if (self.storageChecked) { resolve(); }
+define("app",["require", "exports", "module", "stop-recording-event", "views/notification", "views/loading-screen", "lib/orientation", "lib/bind-all", "debug", "lib/pinch", "lib/bind", "model"],function(require, exports, module) {
+'use strict';
+
+/**
+ * Dependencies
+ */
+
+var stopRecordingEvent = require('stop-recording-event');
+var NotificationView = require('views/notification');
+var LoadingView = require('views/loading-screen');
+var orientation = require('lib/orientation');
+var bindAll = require('lib/bind-all');
+var debug = require('debug')('app');
+var Pinch = require('lib/pinch');
+var bind = require('lib/bind');
+var model = require('model');
+
+/**
+ * Exports
+ */
+
+module.exports = App;
+
+/**
+ * Mixin `Model` API
+ */
+
+model(App.prototype);
+
+/**
+ * Initialize a new `App`
+ *
+ * Options:
+ *
+ * - `root` The node to inject content into
+ *
+ * @param {Object} options
+ * @constructor
+ */
+function App(options) {
+ debug('initialize');
+ bindAll(this);
+ this.views = {};
+ this.dynamicLazy = {};
+ this.el = options.el;
+ this.win = options.win;
+ this.doc = options.doc;
+ this.perf = options.perf || {};
+ this.pinch = options.pinch || new Pinch(this.el); // Test hook
+ this.require = options.require || window.requirejs; // Test hook
+ this.LoadingView = options.LoadingView || LoadingView; // test hook
+ this.orientation = options.orientation || orientation; // test hook
+ this.inSecureMode = (this.win.location.hash === '#secure');
+ this.controllers = options.controllers;
+ this.geolocation = options.geolocation;
+ this.settings = options.settings;
+ this.camera = options.camera;
+ this.activity = {};
+ this.sounds = options.sounds;
+ debug('initialized');
+}
+
+/**
+ * Runs all the methods to boot the app.
+ *
+ * The loading screen is shown until the
+ * camera is 'ready', then it is taken down.
+ *
+ * @public
+ */
+App.prototype.boot = function() {
+ debug('boot');
+ if (this.booted) { return; }
+ this.showSpinner('requestingCamera');
+ this.bindEvents();
+ this.initializeViews();
+ this.runControllers();
+
+ // PERFORMANCE MARKER (1): navigationLoaded
+ // Designates that the app's *core* chrome or navigation interface
+ // exists in the DOM and is marked as ready to be displayed.
+ // PERFORMANCE MARKER (2): navigationInteractive
+ // Designates that the app's *core* chrome or navigation interface
+ // has its events bound and is ready for user interaction.
+ window.performance.mark('navigationLoaded');
+ window.performance.mark('navigationInteractive');
+
+ this.injectViews();
+ this.booted = true;
+ debug('booted');
+};
+
+App.prototype.dispatchEvent = function(name) {
+ this.win.dispatchEvent(new CustomEvent(name));
+};
+
+/**
+ * Runs controllers to glue all
+ * the parts of the app together.
+ *
+ * @private
+ */
+App.prototype.runControllers = function() {
+ debug('run controllers');
+ this.controllers.overlay(this);
+ this.controllers.battery(this);
+ this.controllers.settings(this);
+ this.controllers.activity(this);
+ this.controllers.camera(this);
+ this.controllers.viewfinder(this);
+ this.controllers.hud(this);
+ this.controllers.controls(this);
+ this.controllers.storage(this);
+ debug('controllers run');
+};
+
+/**
+ * Load an extra lazy controller. Used for
+ * controllers only needed based on configuration.
+ *
+ * @param {String} controller
+ */
+App.prototype.loadLazyController = function(controller) {
+ if (!this.criticalPathDone) {
+ this.controllers.lazy.push(controller);
+ return;
+ }
+ if (!this.dynamicLazy[controller]) {
+ this.dynamicLazy[controller] = this.loadLazyControllers([controller]);
+ }
+ return this.dynamicLazy[controller];
+};
+
+/**
+ * Load and run all the lazy controllers.
+ *
+ * @param {Function} done
+ */
+App.prototype.loadLazyControllers = function(controllers) {
+ debug('load lazy controllers');
+ var self = this;
+ return new Promise(function(resolve, reject) {
+ self.require(controllers, function() {
+ [].forEach.call(arguments, function(controller) { controller(self); });
+ debug('controllers loaded');
+ self.emit('lazyloaded');
+ resolve();
+ });
+ });
+};
+
+/**
+ * Initialize views.
+ *
+ * @private
+ */
+App.prototype.initializeViews = function() {
+ debug('initializing views');
+ this.views.notification = new NotificationView();
+ debug('views initialized');
+};
+
+/**
+ * Put views in the DOM.
+ *
+ * @private
+ */
+App.prototype.injectViews = function() {
+ debug('injecting views');
+ this.views.notification.appendTo(this.el);
+ debug('views injected');
+};
+
+/**
+ * Attaches event handlers.
+ *
+ * @private
+ */
+App.prototype.bindEvents = function() {
+ debug('binding events');
+
+ // App
+ this.once('storage:checked:healthy', this.geolocationWatch);
+ this.on('storage:checked', function() { this.storageChecked = true; }.bind(this));
+ this.once('viewfinder:visible', this.onCriticalPathDone);
+ this.once('camera:error', this.onCriticalPathDone);
+ this.once('activity', this.onActivity);
+ this.once('newthumbnail', this.onNewThumbnail);
+ this.once('preview', this.onPreview);
+ this.once('camera:willchange', this.onWillChange);
+ this.on('camera:willchange', this.firer('busy'));
+ this.on('ready', this.clearSpinner);
+ this.on('visible', this.onVisible);
+ this.on('hidden', this.onHidden);
+ this.on('reboot', this.onReboot);
+ this.on('busy', this.onBusy);
+
+ // Settings
+ if (this.settings.countdown.selected('key') !== 'off') {
+ this.onCountdown();
+ } else {
+ this.settings.countdown.once('change:selected', this.onCountdown);
+ }
+
+ // Pinch
+ this.pinch.on('changed', this.firer('pinch:changed'));
+ this.on('previewgallery:opened', this.pinch.disable);
+ this.on('previewgallery:closed', this.pinch.enable);
+ this.on('settings:opened', this.pinch.disable);
+ this.on('settings:closed', this.pinch.enable);
+
+ // DOM
+ bind(this.doc, 'visibilitychange', this.onVisibilityChange);
+
+ // we bind to window.onlocalized in order not to depend
+ // on l10n.js loading (which is lazy). See bug 999132
+ bind(this.doc, 'DOMRetranslated', this.firer('localized'));
+
+ bind(this.win, 'beforeunload', this.onBeforeUnload);
+ bind(this.win, 'keydown', this.onKeyDown);
+ bind(this.el, 'click', this.onClick);
+ debug('events bound');
+};
+
+/**
+ * Tasks to run when the
+ * app becomes visible.
+ *
+ * Check the storage again as users
+ * may have made changes since the
+ * app was minimised
+ */
+App.prototype.onVisible = function() {
+ this.geolocationWatch();
+ this.orientation.start();
+ this.orientation.lock();
+ debug('visible');
+};
+
+/**
+ * Tasks to run when the
+ * app is minimised/hidden.
+ *
+ * @private
+ */
+App.prototype.onHidden = function() {
+ this.geolocation.stopWatching();
+ this.orientation.stop();
+ debug('hidden');
+};
+
+
+/**
+ * Reboots the application
+ *
+ * @private
+ */
+App.prototype.onReboot = function() {
+ debug('reboot');
+ window.location.reload();
+};
+
+
+/**
+ * Emit a click event that other
+ * modules can listen to.
+ *
+ * @private
+ */
+App.prototype.onClick = function() {
+ debug('click');
+ this.emit('click');
+};
+
+/**
+ * Log when critical path has completed.
+ *
+ * @private
+ */
+App.prototype.onCriticalPathDone = function() {
+ if (this.criticalPathDone) { return; }
+ debug('critical path done');
+ // PERFORMANCE MARKER (3): visuallyLoaded
+ // Designates that the app is visually loaded (e.g.: all of the
+ // "above-the-fold" content exists in the DOM and is marked as
+ // ready to be displayed).
+ window.performance.mark('visuallyLoaded');
+
+ // Load non-critical modules
+ this.listenForStopRecordingEvent();
+ this.loadLazyModules();
+ this.perf.criticalPath = Date.now();
+ this.criticalPathDone = true;
+ this.emit('criticalpathdone');
+};
+
+/**
+ * Lazy load activity dependencies.
+ *
+ * @private
+ */
+App.prototype.onActivity = function() {
+ this.loadLazyController('controllers/confirm');
+};
+
+App.prototype.onCountdown = function() {
+ this.loadLazyController('controllers/countdown');
+};
+
+App.prototype.onNewThumbnail = function() {
+ this.loadLazyController('controllers/preview-gallery');
+};
+
+App.prototype.onPreview = function() {
+ var self = this;
+ this.emit('busy', 'lazyLoading');
+ this.loadLazyController('controllers/preview-gallery').then(function() {
+ self.emit('preview');
+ self.emit('ready');
+ });
+};
+
+App.prototype.onWillChange = function() {
+ this.loadLazyController('controllers/recording-timer');
+};
+
+App.prototype.loadLazyModules = function() {
+ debug('load lazy modules');
+ var self = this;
+
+ var load = this.loadLazyControllers(this.controllers.lazy);
+ var storage = new Promise(function(resolve, reject) {
+ if (self.storageChecked) { resolve(); }
else { self.once('storage:checked', resolve); }
- });
- Promise.all([
- load,
- storage
- ]).then(function () {
- debug('app fully loaded');
- window.performance.mark('contentInteractive');
- self.loaded = true;
- self.emit('loaded');
- self.perf.loaded = Date.now();
- self.logPerf();
- window.performance.mark('fullyLoaded');
- });
- };
- App.prototype.logPerf = function () {
- var timing = window.performance.timing;
- console.log('first module: %s', this.perf.firstModule - this.perf.jsStarted + 'ms');
- console.log('critical-path: %s', this.perf.criticalPath - timing.domLoading + 'ms');
- console.log('app-fully-loaded: %s', this.perf.loaded - timing.domLoading + 'ms');
- };
- App.prototype.geolocationWatch = function () {
- var shouldWatch = !this.activity.pick && !this.hidden;
- if (shouldWatch) {
- this.geolocation.watch();
- }
- };
- App.prototype.onVisibilityChange = function () {
- this.hidden = this.doc.hidden;
- this.emit(this.hidden ? 'hidden' : 'visible');
- };
- App.prototype.onBeforeUnload = function () {
- this.emit('beforeunload');
- debug('beforeunload');
- };
- App.prototype.showSpinner = function (key) {
- debug('show loading type: %s', key);
- this.busy = true;
- var view = this.views.loading;
- if (view) {
- return;
- }
- var ms = this.settings.spinnerTimeouts.get(key) || 0;
- var self = this;
- clearTimeout(this.spinnerTimeout);
- this.spinnerTimeout = setTimeout(function () {
- self.views.loading = new self.LoadingView();
- self.views.loading.appendTo(self.el).show();
- debug('loading shown');
- }, ms);
- };
- App.prototype.clearSpinner = function () {
- debug('clear loading');
- this.busy = false;
- var view = this.views.loading;
- clearTimeout(this.spinnerTimeout);
- if (!view) {
- return;
- }
- view.hide(view.destroy);
- this.views.loading = null;
- };
- App.prototype.onBusy = function (type) {
- debug('camera busy, type: %s', type);
- this.busy = true;
- var delay = this.settings.spinnerTimeouts.get(type);
- if (delay) {
- this.showSpinner(type);
- }
- };
- App.prototype.listenForStopRecordingEvent = function () {
- debug('listen for stop recording events');
- stopRecordingEvent.start();
- addEventListener('stoprecording', this.firer('stoprecording'));
- };
- App.prototype.onKeyDown = function (e) {
- var key = e.key.toLowerCase();
- var type = this.settings.keyDownEvents.get(key);
- if (type) {
- this.emit('keydown:' + type, e);
- }
- };
+ });
+
+ // All done
+ Promise.all([load, storage]).then(function() {
+ debug('app fully loaded');
+
+ // PERFORMANCE MARKER (4): contentInteractive
+ // Designates that the app has its events bound for the minimum
+ // set of functionality to allow the user to interact with the
+ // "above-the-fold" content.
+ window.performance.mark('contentInteractive');
+
+ self.loaded = true;
+ self.emit('loaded');
+ self.perf.loaded = Date.now();
+ self.logPerf();
+
+ // PERFORMANCE MARKER (5): fullyLoaded
+ // Designates that the app is *completely* loaded and all relevant
+ // "below-the-fold" content exists in the DOM, is marked visible,
+ // has its events bound and is ready for user interaction. All
+ // required startup background processing should be complete.
+ window.performance.mark('fullyLoaded');
+ });
+};
+
+App.prototype.logPerf =function() {
+ var timing = window.performance.timing;
+ console.log('first module: %s',
+ this.perf.firstModule - this.perf.jsStarted + 'ms');
+ console.log('critical-path: %s',
+ this.perf.criticalPath - timing.domLoading + 'ms');
+ console.log('app-fully-loaded: %s',
+ this.perf.loaded - timing.domLoading + 'ms');
+};
+
+/**
+ * Begins watching location if not within
+ * a pending activity and the app isn't
+ * currently hidden.
+ *
+ * Watching is delayed by the `promptDelay`
+ * defined in settings.
+ *
+ * @private
+ */
+App.prototype.geolocationWatch = function() {
+ var shouldWatch = !this.activity.pick && !this.hidden;
+ if (shouldWatch) { this.geolocation.watch(); }
+};
+
+/**
+ * Responds to the `visibilitychange`
+ * event, emitting useful app events
+ * that allow us to perform relate
+ * work elsewhere.
+ *
+ * @private
+ */
+App.prototype.onVisibilityChange = function() {
+ this.hidden = this.doc.hidden;
+ this.emit(this.hidden ? 'hidden' : 'visible');
+};
+
+/**
+ * Runs just before the
+ * app is destroyed.
+ *
+ * @private
+ */
+App.prototype.onBeforeUnload = function() {
+ this.emit('beforeunload');
+ debug('beforeunload');
+};
+
+/**
+ * Shows the loading screen after the
+ * number of ms defined in config.js
+ *
+ * @param {String} type
+ * @private
+ */
+App.prototype.showSpinner = function(key) {
+ debug('show loading type: %s', key);
+ this.busy = true;
+
+ var view = this.views.loading;
+ if (view) {
+ return;
+ }
+
+ var ms = this.settings.spinnerTimeouts.get(key) || 0;
+ var self = this;
+
+ clearTimeout(this.spinnerTimeout);
+ this.spinnerTimeout = setTimeout(function() {
+ self.views.loading = new self.LoadingView();
+ self.views.loading.appendTo(self.el).show();
+ debug('loading shown');
+ }, ms);
+};
+
+/**
+ * Clears the loadings screen, or
+ * any pending loading screen.
+ *
+ * @private
+ */
+App.prototype.clearSpinner = function() {
+ debug('clear loading');
+ this.busy = false;
+ var view = this.views.loading;
+ clearTimeout(this.spinnerTimeout);
+ if (!view) { return; }
+ view.hide(view.destroy);
+ this.views.loading = null;
+};
+
+/**
+ * When the camera indicates it's busy it
+ * sometimes passes a `type` string. When
+ * this type matches one of our keys in the
+ * `spinnerTimeouts` config, we display the
+ * loading screen passing on the type.
+ *
+ * @param {String} type
+ * @private
+ */
+App.prototype.onBusy = function(type) {
+ debug('camera busy, type: %s', type);
+ this.busy = true;
+ var delay = this.settings.spinnerTimeouts.get(type);
+ if (delay) { this.showSpinner(type); }
+};
+
+/**
+ * If the system app is opening an attention screen (because
+ * of an incoming call or an alarm, e.g.) and if we are
+ * currently recording a video then we need to stop recording
+ * before the ringer or alarm starts sounding. We will be sent
+ * to the background shortly after this and will stop recording
+ * when that happens, but by that time it is too late and we
+ * have already recorded some sound. See bugs 995540 and 1006200.
+ *
+ * Similarly, if the user presses the Home button or switches to
+ * another app while recording, we need to stop recording right away,
+ * even if the system is overloaded and we don't get a visiblitychange
+ * event right away. See bug 1046167.
+ *
+ * To make this work, we rely on shared/js/stop_recording_event.js which
+ * abuses the settings API to allow the system app to broadcast a "you
+ * will soon be hidden" message to any certified apps that care. There
+ * ought to be a better way, but this is a quick way to fix a
+ * last-minute release blocker.
+ *
+ * @private
+ */
+App.prototype.listenForStopRecordingEvent = function() {
+ debug('listen for stop recording events');
+
+ // Start the module that generates
+ // the stoprecording events and listen
+ // for those custom DOM events and emit
+ // them using our internal event emitter
+ stopRecordingEvent.start();
+ addEventListener('stoprecording', this.firer('stoprecording'));
+};
+
+
+/**
+ * When the device's hardware keys
+ * are pressed we emit a global
+ * app event that other controllers
+ * can respond to.
+ *
+ * TIP: Check config.js for the map
+ * of key names to types.
+ *
+ * @param {Event} e
+ * @private
+ */
+App.prototype.onKeyDown = function(e) {
+ var key = e.key.toLowerCase();
+ var type = this.settings.keyDownEvents.get(key);
+ if (type) { this.emit('keydown:' + type, e); }
+};
+
+
});
-define('controllers/overlay', [
- 'require',
- 'exports',
- 'module',
- 'debug',
- 'lib/bind-all'
-], function (require, exports, module) {
- 'use strict';
- var debug = require('debug')('controller:overlay');
- var bindAll = require('lib/bind-all');
- module.exports = function (app) {
- return new OverlayController(app);
+define("controllers/overlay",["require", "exports", "module", "debug", "lib/bind-all"],function(require, exports, module) {
+'use strict';
+
+/**
+ * Dependencies
+ */
+
+var debug = require('debug')('controller:overlay');
+var bindAll = require('lib/bind-all');
+
+/**
+ * Exports
+ */
+
+module.exports = function(app) { return new OverlayController(app); };
+module.exports.OverlayController = OverlayController;
+
+/**
+ * Initialize a new `OverlayController`
+ *
+ * @param {App} app
+ */
+function OverlayController(app) {
+ bindAll(this);
+ this.overlays = {};
+ this.app = app;
+ this.require = app.require;
+ this.activity = app.activity;
+ this.bindEvents();
+ debug('initialized');
+}
+
+OverlayController.prototype.bindEvents = function() {
+ this.app.on('storage:changed', this.onStorageChanged);
+ this.app.on('change:batteryStatus', this.onBatteryChanged);
+ this.app.on('camera:requesting', this.onCameraRequesting);
+ this.app.on('camera:error', this.onCameraError);
+};
+
+/**
+ * Respond to storage `statechange`
+ * events by inserting or destroying
+ * overlays from the app.
+ *
+ * @param {String} value ['nospace'|'shared'|'unavailable'|'available']
+ */
+OverlayController.prototype.onStorageChanged = function(state) {
+ this.updateOverlay('storage', state !== 'available', state);
+};
+
+/**
+ * Respond to battery `statuschange`
+ * events by inserting or destroying
+ * overlays from the app.
+ *
+ * @param {String} status ['shutdown'|'critical'|'verylow'|'low']
+ */
+OverlayController.prototype.onBatteryChanged = function(state) {
+ this.updateOverlay('battery', state === 'shutdown', state);
+};
+
+/**
+ * Respond to camera `requesting`
+ * events by destroying overlays
+ * from the app.
+ *
+ * @param {String} state ['start'|'success'|'fail']
+ */
+OverlayController.prototype.onCameraRequesting = function() {
+ this.updateOverlay('cameraError', false);
+};
+
+/**
+ * Respond to camera `error`
+ * events by inserting overlays
+ * into the app.
+ *
+ * @param {String} state ['start'|'success'|'fail']
+ */
+OverlayController.prototype.onCameraError = function(type) {
+ this.updateOverlay('cameraError', true, type);
+};
+
+OverlayController.prototype.updateOverlay = function(type, enabled, reason) {
+ debug('\'%s/%s\' overlay %s', type, reason, enabled);
+
+ var overlay = this.overlays[type];
+ if (!overlay) {
+ overlay = this.overlays[type] = {
+ id: 0,
};
- module.exports.OverlayController = OverlayController;
- function OverlayController(app) {
- bindAll(this);
- this.overlays = {};
- this.app = app;
- this.require = app.require;
- this.activity = app.activity;
- this.bindEvents();
- debug('initialized');
+ }
+
+ if (overlay.view) {
+ overlay.view.destroy();
+ delete overlay.view;
+ }
+
+ var id = ++overlay.id;
+ if (!enabled) {
+ return;
+ }
+
+ var self = this;
+ this.require(['views/overlay'], function(OverlayView) {
+ if (id !== overlay.id) {
+ return;
}
- OverlayController.prototype.bindEvents = function () {
- this.app.on('storage:changed', this.onStorageChanged);
- this.app.on('change:batteryStatus', this.onBatteryChanged);
- this.app.on('camera:requesting', this.onCameraRequesting);
- this.app.on('camera:error', this.onCameraError);
- };
- OverlayController.prototype.onStorageChanged = function (state) {
- this.updateOverlay('storage', state !== 'available', state);
- };
- OverlayController.prototype.onBatteryChanged = function (state) {
- this.updateOverlay('battery', state === 'shutdown', state);
- };
- OverlayController.prototype.onCameraRequesting = function () {
- this.updateOverlay('cameraError', false);
- };
- OverlayController.prototype.onCameraError = function (type) {
- this.updateOverlay('cameraError', true, type);
- };
- OverlayController.prototype.updateOverlay = function (type, enabled, reason) {
- debug('\'%s/%s\' overlay %s', type, reason, enabled);
- var overlay = this.overlays[type];
- if (!overlay) {
- overlay = this.overlays[type] = { id: 0 };
+
+ var closable = self.activity.pick || reason === 'request-fail';
+ var view = new OverlayView({
+ type: reason,
+ closable: closable,
+ });
+ if (!view.rendered()) {
+ return;
+ }
+
+ overlay.view = view.appendTo(document.body)
+ .on('click:close-btn', function() {
+ if (reason === 'request-fail' && !self.activity.pick) {
+ self.updateOverlay(type, false);
+ return;
}
- if (overlay.view) {
- overlay.view.destroy();
- delete overlay.view;
- }
- var id = ++overlay.id;
- if (!enabled) {
- return;
- }
- var self = this;
- this.require(['views/overlay'], function (OverlayView) {
- if (id !== overlay.id) {
- return;
- }
- var closable = self.activity.pick || reason === 'request-fail';
- var view = new OverlayView({
- type: reason,
- closable: closable
- });
- if (!view.rendered()) {
- 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);
- });
- };
+ self.app.emit('activitycanceled');
+ });
+
+ debug('inserted \'%s/%s\' overlay', type, reason);
+ });
+};
+
});
-define('controllers/battery', [
- 'require',
- 'exports',
- 'module',
- 'debug',
- 'lib/bind-all',
- 'lib/bind'
-], function (require, exports, module) {
- var debug = require('debug')('controller:battery');
- var bindAll = require('lib/bind-all');
- var bind = require('lib/bind');
- module.exports = function (app) {
- return new BatteryController(app);
- };
- module.exports.BatteryController = BatteryController;
- function BatteryController(app) {
- bindAll(this);
- this.app = app;
- this.battery = app.battery || navigator.battery || navigator.mozBattery;
+define("controllers/battery",["require", "exports", "module", "debug", "lib/bind-all", "lib/bind"],function(require, exports, module) {
+'use strict';
+
+/**
+ * Dependencies
+ */
+
+var debug = require('debug')('controller:battery');
+var bindAll = require('lib/bind-all');
+var bind = require('lib/bind');
+
+/**
+ * Exports
+ */
+
+module.exports = function(app) { return new BatteryController(app); };
+module.exports.BatteryController = BatteryController;
+
+/**
+ * Initialize a new `BatteryController`
+ *
+ * @param {Object} options
+ */
+function BatteryController(app) {
+ bindAll(this);
+ this.app = app;
+ this.battery = app.battery || navigator.battery || navigator.mozBattery;
if (!this.battery) { this.app.set('batteryStatus', 'unknown'); return; }
- this.levels = app.settings.battery.get('levels');
- this.notification = app.views.notification;
- this.bindEvents();
- this.updateStatus();
- debug('initialized');
+ this.levels = app.settings.battery.get('levels');
+ this.notification = app.views.notification;
+ this.bindEvents();
+ this.updateStatus();
+ debug('initialized');
+}
+
+/**
+ * Bind callbacks to required events.
+ *
+ * @private
+ */
+BatteryController.prototype.bindEvents = function() {
+ bind(this.battery, 'levelchange', this.updateStatus);
+ bind(this.battery, 'chargingchange', this.updateStatus);
+ this.app.on('change:batteryStatus', this.onStatusChange);
+ this.app.on('change:recording', this.updatePowerSave);
+
+ var mozSettings = navigator.mozSettings;
+ mozSettings.addObserver('powersave.enabled', this.onPowerSaveChange);
+ mozSettings.createLock().get('powersave.enabled').then(
+ this.onPowerSaveChange);
+};
+
+/**
+ * Callback from settings when the power save state changes.
+ *
+ * @private
+ */
+BatteryController.prototype.onPowerSaveChange = function(values) {
+ var value;
+ if (values.settingValue !== undefined) {
+ value = values.settingValue;
+ } else {
+ value = values['powersave.enabled'];
+ }
+ this.powerSaveEnabled = value;
+ this.updatePowerSave();
+};
+
+/**
+ * Emits powersave event if state has changed.
+ *
+ * @private
+ */
+BatteryController.prototype.updatePowerSave = function() {
+ var state = this.powerSaveEnabled && !this.app.get('recording');
+ if (this.powerSave === state) {
+ return;
+ }
+
+ this.powerSave = state;
+ debug('power save: ' + state);
+ this.app.emit('battery:powersave', state);
+};
+
+/**
+ * Map of status keys to message.
+ *
+ * @type {Object}
+ * @private
+ */
+BatteryController.prototype.notifications = {
+ low: {
+ text: 'battery-low-text',
+ attrs: {
+ 'data-icon': 'battery-3',
+ 'data-l10n-id': 'battery-low-indicator'
+ },
+ },
+ verylow: {
+ text: 'battery-verylow-text',
+ attrs: {
+ 'data-icon': 'battery-1',
+ 'data-l10n-id': 'battery-verylow-indicator'
}
- BatteryController.prototype.bindEvents = function () {
- bind(this.battery, 'levelchange', this.updateStatus);
- bind(this.battery, 'chargingchange', this.updateStatus);
- this.app.on('change:batteryStatus', this.onStatusChange);
- this.app.on('change:recording', this.updatePowerSave);
- var mozSettings = navigator.mozSettings;
- mozSettings.addObserver('powersave.enabled', this.onPowerSaveChange);
- mozSettings.createLock().get('powersave.enabled').then(this.onPowerSaveChange);
- };
- BatteryController.prototype.onPowerSaveChange = function (values) {
- var value;
- if (values.settingValue !== undefined) {
- value = values.settingValue;
- } else {
- value = values['powersave.enabled'];
- }
- this.powerSaveEnabled = value;
- this.updatePowerSave();
- };
- BatteryController.prototype.updatePowerSave = function () {
- var state = this.powerSaveEnabled && !this.app.get('recording');
- if (this.powerSave === state) {
- return;
- }
- this.powerSave = state;
- debug('power save: ' + state);
- this.app.emit('battery:powersave', state);
- };
- BatteryController.prototype.notifications = {
- low: {
- text: 'battery-low-text',
- attrs: {
- 'data-icon': 'battery-3',
- 'data-l10n-id': 'battery-low-indicator'
- }
- },
- verylow: {
- text: 'battery-verylow-text',
- attrs: {
- 'data-icon': 'battery-1',
- 'data-l10n-id': 'battery-verylow-indicator'
- }
- },
- critical: {
- text: 'battery-critical-text',
- attrs: {
- 'data-icon': 'battery-1',
- 'data-l10n-id': 'battery-critical-indicator'
- },
- persistent: true
- }
- };
- BatteryController.prototype.updateStatus = function () {
- var previous = this.app.get('batteryStatus');
- var current = this.getStatus(this.battery);
- if (current !== previous) {
- this.app.set('batteryStatus', current);
- }
- };
- BatteryController.prototype.getStatus = function (battery) {
- var level = Math.round(battery.level * 100);
- var levels = this.levels;
- if (battery.charging) {
- return 'charging';
- } else if (level <= levels.shutdown) {
- return 'shutdown';
- } else if (level <= levels.critical) {
- return 'critical';
- } else if (level <= levels.verylow) {
- return 'verylow';
- } else if (level <= levels.low) {
- return 'low';
- } else {
- return 'healthy';
- }
- };
- BatteryController.prototype.onStatusChange = function (status) {
- this.clearLastNotification();
- this.displayNotification(status);
- };
- BatteryController.prototype.displayNotification = function (status) {
- var notification = this.notifications[status];
- if (!notification) {
- return;
- }
- this.lastNotification = this.notification.display({
- text: notification.text,
- className: notification.className,
- attrs: notification.attrs,
- persistent: notification.persistent
- });
- };
- BatteryController.prototype.clearLastNotification = function () {
- this.notification.clear(this.lastNotification);
- };
+ },
+ critical: {
+ text: 'battery-critical-text',
+ attrs: {
+ 'data-icon': 'battery-1',
+ 'data-l10n-id': 'battery-critical-indicator'
+ },
+ persistent: true
+ }
+};
+
+/**
+ * Updates app `batteryStatus` and
+ * manages battery notifications.
+ *
+ * @private
+ */
+BatteryController.prototype.updateStatus = function () {
+ var previous = this.app.get('batteryStatus');
+ var current = this.getStatus(this.battery);
+ if (current !== previous) {
+ this.app.set('batteryStatus', current);
+ }
+};
+
+/**
+ * Returns a status key derived
+ * from the given `battery` object.
+ *
+ * @param {Battery} battery
+ * @return {String}
+ * @private
+ */
+BatteryController.prototype.getStatus = function(battery) {
+ var level = Math.round(battery.level * 100);
+ var levels = this.levels;
+
+ if (battery.charging) { return 'charging'; }
+ else if (level <= levels.shutdown) { return 'shutdown'; }
+ else if (level <= levels.critical) { return 'critical'; }
+ else if (level <= levels.verylow) { return 'verylow'; }
+ else if (level <= levels.low) { return 'low'; }
+ else { return 'healthy'; }
+};
+
+BatteryController.prototype.onStatusChange = function(status) {
+ this.clearLastNotification();
+ this.displayNotification(status);
+};
+
+BatteryController.prototype.displayNotification = function(status) {
+ var notification = this.notifications[status];
+ if (!notification) { return; }
+
+ this.lastNotification = this.notification.display({
+ text: notification.text,
+ className: notification.className,
+ attrs: notification.attrs,
+ persistent: notification.persistent
+ });
+};
+
+BatteryController.prototype.clearLastNotification = function() {
+ this.notification.clear(this.lastNotification);
+};
+
});
define('views/hud', [
'require',
@@ -5324,185 +6347,320 @@ define('lib/camera-coordinates', [
}
};
});
-define('views/viewfinder', [
- 'require',
- 'exports',
- 'module',
- 'debug',
- 'lib/bind',
- 'lib/camera-utils',
- 'view'
-], function (require, exports, module) {
- 'use strict';
- var debug = require('debug')('view:viewfinder');
- var bind = require('lib/bind');
- var CameraUtils = require('lib/camera-utils');
- var View = require('view');
- var isZoomEnabled = false;
- var scaleSizeTo = {
- fill: CameraUtils.scaleSizeToFillViewport,
- fit: CameraUtils.scaleSizeToFitViewport
+define("views/viewfinder",["require", "exports", "module", "debug", "lib/bind", "lib/camera-utils", "view"],function(require, exports, module) {
+'use strict';
+
+/**
+ * Dependencies
+ */
+
+var debug = require('debug')('view:viewfinder');
+var bind = require('lib/bind');
+var CameraUtils = require('lib/camera-utils');
+var View = require('view');
+
+/**
+ * Locals
+ */
+
+var isZoomEnabled = false;
+var scaleSizeTo = {
+ fill: CameraUtils.scaleSizeToFillViewport,
+ fit: CameraUtils.scaleSizeToFitViewport
+};
+
+var clamp = function(value, minimum, maximum) {
+ return Math.min(Math.max(value, minimum), maximum);
+};
+
+module.exports = View.extend({
+ name: 'viewfinder',
+ className: 'js-viewfinder',
+ fadeTime: 360,
+
+ initialize: function() {
+ this.render();
+ this.getSize();
+ },
+
+ render: function() {
+ this.el.innerHTML = this.template();
+ this.els.frame = this.find('.js-frame');
+ this.els.video = this.find('.js-video');
+ this.els.videoContainer = this.find('.js-video-container');
+
+ // Clean up
+ delete this.template;
+
+ debug('rendered');
+ return this.bindEvents();
+ },
+
+ bindEvents: function() {
+ bind(this.el, 'click', this.onClick);
+ bind(this.el, 'animationend', this.onShutterEnd);
+ return this;
+ },
+
+ /**
+ * Stores the container size.
+ *
+ * We're using window dimensions
+ * here to avoid causing costly
+ * reflows.
+ *
+ * @public
+ */
+ getSize: function() {
+ this.width = window.innerWidth;
+ this.height = window.innerHeight;
+ return {
+ width: this.width,
+ height: this.height
};
- var clamp = function (value, minimum, maximum) {
- return Math.min(Math.max(value, minimum), maximum);
- };
- module.exports = View.extend({
- name: 'viewfinder',
- className: 'js-viewfinder',
- fadeTime: 360,
- initialize: function () {
- this.render();
- this.getSize();
- },
- render: function () {
- this.el.innerHTML = this.template();
- this.els.frame = this.find('.js-frame');
- this.els.video = this.find('.js-video');
- this.els.videoContainer = this.find('.js-video-container');
- delete this.template;
- debug('rendered');
- return this.bindEvents();
- },
- bindEvents: function () {
- bind(this.el, 'click', this.onClick);
- bind(this.el, 'animationend', this.onShutterEnd);
- return this;
- },
- getSize: function () {
- this.width = window.innerWidth;
- this.height = window.innerHeight;
- return {
- width: this.width,
- height: this.height
- };
- },
- onClick: function (e) {
- this.emit('click', e);
- },
- enableZoom: function (minimumZoom, maximumZoom) {
- if (minimumZoom) {
- this._minimumZoom = minimumZoom;
- }
- if (maximumZoom) {
- this._maximumZoom = maximumZoom;
- }
- isZoomEnabled = true;
- },
- disableZoom: function () {
- this._minimumZoom = 1;
- this._maximumZoom = 1;
- this.setZoom(1);
- isZoomEnabled = false;
- },
- _minimumZoom: 1,
- setMinimumZoom: function (minimumZoom) {
- this._minimumZoom = minimumZoom;
- },
- _maximumZoom: 1,
- setMaximumZoom: function (maximumZoom) {
- this._maximumZoom = maximumZoom;
- },
- _zoom: 1,
- setZoom: function (zoom) {
- if (!isZoomEnabled) {
- return;
- }
- this._zoom = clamp(zoom, this._minimumZoom, this._maximumZoom);
- },
- _useZoomPreviewAdjustment: false,
- enableZoomPreviewAdjustment: function () {
- this._useZoomPreviewAdjustment = true;
- },
- disableZoomPreviewAdjustment: function () {
- this._useZoomPreviewAdjustment = false;
- },
- setZoomPreviewAdjustment: function (zoomPreviewAdjustment) {
- if (this._useZoomPreviewAdjustment) {
- this.els.video.style.transform = 'scale(' + zoomPreviewAdjustment + ')';
- }
- },
- stopStream: function () {
- this.els.video.mozSrcObject = null;
- },
- fadeOut: function () {
- debug('fade-out');
- var self = this;
- this.hide();
- clearTimeout(this.fadeTimeout);
- this.fadeTimeout = setTimeout(function () {
- self.emit('fadedout');
- }, this.fadeTime);
- },
- fadeIn: function (firstRun) {
- debug('fade-in');
- this.show();
- if (firstRun) {
- this.emit('fadedin');
- requestAnimationFrame(() => {
- this.el.style.transitionDuration = this.fadeTime + 'ms';
- });
- } else {
- this.fadeTimeout = setTimeout(this.firer('fadedin'), this.fadeTime);
- }
- },
- shutter: function () {
- this.el.classList.add('shutter');
- },
- onShutterEnd: function () {
- this.reflow = this.el.offsetTop;
- this.el.classList.remove('shutter');
- },
- updatePreview: function (preview, sensorAngle, mirrored) {
- 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 = {
- width: this.width,
- height: this.height,
- aspect: this.width / this.height
- };
- aspect = preview.height / preview.width;
- } else {
- this.container = {
- width: this.height,
- height: this.width,
- aspect: this.height / this.width
- };
- aspect = preview.width / preview.height;
- }
- var shouldFill = aspect > this.container.aspect;
- var scaleType = this.scaleType || (shouldFill ? 'fill' : 'fit');
- this.updatePreviewMetrics(preview, sensorAngle, mirrored, scaleType);
- },
- updatePreviewMetrics: function (preview, sensorAngle, mirrored, scaleType) {
- debug('update preview scaleType: %s', scaleType, preview);
- var landscape = scaleSizeTo[scaleType](this.container, preview);
- var portrait = {
- width: landscape.height,
- height: landscape.width
- };
- this.els.frame.style.width = portrait.width + 'px';
- this.els.frame.style.height = portrait.height + 'px';
- var transform = '';
- if (mirrored) {
- transform += 'scale(-1, 1) ';
- }
- transform += 'rotate(' + sensorAngle + 'deg)';
- this.els.videoContainer.style.width = landscape.width + 'px';
- this.els.videoContainer.style.height = landscape.height + 'px';
- this.els.videoContainer.style.transform = transform;
- this.set('scaleType', scaleType);
- debug('updated preview size/position', landscape);
- },
- template: function () {
- return '' + '
' + '' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '';
- }
- });
+ },
+
+ onClick: function(e) {
+ this.emit('click', e);
+ },
+
+ enableZoom: function(minimumZoom, maximumZoom) {
+ if (minimumZoom) {
+ this._minimumZoom = minimumZoom;
+ }
+
+ if (maximumZoom) {
+ this._maximumZoom = maximumZoom;
+ }
+
+ isZoomEnabled = true;
+ },
+
+ disableZoom: function() {
+ this._minimumZoom = 1.0;
+ this._maximumZoom = 1.0;
+
+ this.setZoom(1.0);
+
+ isZoomEnabled = false;
+ },
+
+ _minimumZoom: 1.0,
+
+ setMinimumZoom: function(minimumZoom) {
+ this._minimumZoom = minimumZoom;
+ },
+
+ _maximumZoom: 1.0,
+
+ setMaximumZoom: function(maximumZoom) {
+ this._maximumZoom = maximumZoom;
+ },
+
+ _zoom: 1.0,
+
+ /**
+ * Update the internal state of the view so that any future
+ * pinch-to-zoom gestures can correctly adjust the current zoom
+ * level in the event that the zoom level is changed outside of
+ * the pinch-to-zoom gesture (e.g.: ZoomBar). This gets called
+ * when the `Camera` emits a `zoomchanged` event.
+ */
+ setZoom: function(zoom) {
+ if (!isZoomEnabled) {
+ return;
+ }
+
+ this._zoom = clamp(zoom, this._minimumZoom, this._maximumZoom);
+ },
+
+ _useZoomPreviewAdjustment: false,
+
+ enableZoomPreviewAdjustment: function() {
+ this._useZoomPreviewAdjustment = true;
+ },
+
+ disableZoomPreviewAdjustment: function() {
+ this._useZoomPreviewAdjustment = false;
+ },
+
+ /**
+ * Adjust the scale of the tag to compensate for the inability
+ * of the Camera API to zoom the preview stream beyond a certain point.
+ * This gets called when the `Camera` emits a `zoomchanged` event and is
+ * calculated by `Camera.prototype.getZoomPreviewAdjustment()`.
+ */
+ setZoomPreviewAdjustment: function(zoomPreviewAdjustment) {
+ if (this._useZoomPreviewAdjustment) {
+ this.els.video.style.transform = 'scale(' + zoomPreviewAdjustment + ')';
+ }
+ },
+
+ stopStream: function() {
+ this.els.video.mozSrcObject = null;
+ },
+
+ fadeOut: function() {
+ debug('fade-out');
+ var self = this;
+ this.hide();
+ clearTimeout(this.fadeTimeout);
+ this.fadeTimeout = setTimeout(function() {
+ self.emit('fadedout');
+ }, this.fadeTime);
+ },
+
+ fadeIn: function(firstRun) {
+ debug('fade-in');
+ this.show();
+ if (firstRun) {
+ this.emit('fadedin');
+ // Delay to prevent the duration from applying too early
+ requestAnimationFrame(() => {
+ this.el.style.transitionDuration = this.fadeTime + 'ms';
+ });
+ } else {
+ this.fadeTimeout = setTimeout(this.firer('fadedin'), this.fadeTime);
+ }
+ },
+
+ /**
+ * Triggers a quick shutter style animation.
+ *
+ * @private
+ */
+ shutter: function() {
+ this.el.classList.add('shutter');
+ },
+
+ /**
+ * Force a reflow before removing
+ * the shutter class so that it
+ * doesn't impact the animation.
+ *
+ * @private
+ */
+ onShutterEnd: function() {
+ this.reflow = this.el.offsetTop;
+ this.el.classList.remove('shutter');
+ },
+
+ /**
+ * Sizes and positions the preview stream.
+ *
+ * @param {Object} preview
+ * @param {Number} sensorAngle
+ * @param {Boolean} mirrored
+ */
+ 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
+ // 0 or 180 degrees.
+ if (sensorAngle % 180 === 0) {
+ this.container = {
+ width: this.width,
+ height: this.height,
+ aspect: this.width / this.height
+ };
+
+ aspect = preview.height / preview.width;
+ } else {
+ this.container = {
+ width: this.height,
+ height: this.width,
+ aspect: this.height / this.width
+ };
+
+ aspect = preview.width / preview.height;
+ }
+
+ var shouldFill = aspect > this.container.aspect;
+ var scaleType = this.scaleType || (shouldFill ? 'fill' : 'fit');
+
+ this.updatePreviewMetrics(preview, sensorAngle, mirrored, scaleType);
+ },
+
+ /**
+ * Calculates the correct sizing
+ * depending on the chosen 'scaleType'.
+ *
+ * 'scale-type' attribute set as a styling hook.
+ *
+ * @param {Object} preview
+ * @param {Number} sensorAngle
+ * @param {Boolean} mirrored
+ * @param {String} scaleType 'fill'|'fit'
+ */
+ updatePreviewMetrics: function(preview, sensorAngle, mirrored, scaleType) {
+ debug('update preview scaleType: %s', scaleType, preview);
+
+ // Calculate the correct scale to apply to the
+ // preview to either 'fill' or 'fit' the viewfinder
+ // container (always preserving the aspect ratio).
+ var landscape = scaleSizeTo[scaleType](this.container, preview);
+ var portrait = { width: landscape.height, height: landscape.width };
+
+ // Set the size of the frame to match 'portrait' dimensions
+ this.els.frame.style.width = portrait.width + 'px';
+ this.els.frame.style.height = portrait.height + 'px';
+
+ var transform = '';
+ if (mirrored) {
+ transform += 'scale(-1, 1) ';
+ }
+
+ transform += 'rotate(' + sensorAngle + 'deg)';
+
+ // Set the size of the video container to match the
+ // 'landscape' dimensions (CSS is used to rotate
+ // the 'landscape' video stream to 'portrait')
+ this.els.videoContainer.style.width = landscape.width + 'px';
+ this.els.videoContainer.style.height = landscape.height + 'px';
+ this.els.videoContainer.style.transform = transform;
+
+ // CSS aligns the contents slightly
+ // differently depending on the scaleType
+ this.set('scaleType', scaleType);
+
+ debug('updated preview size/position', landscape);
+ },
+
+ template: function() {
+ return '' +
+ '
' +
+ '' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '';
+ }
+});
+
});
define('views/focus', [
'require',
@@ -5647,245 +6805,451 @@ define('views/faces', [
}
});
});
-define('controllers/viewfinder', [
- 'require',
- 'exports',
- 'module',
- 'lib/camera-coordinates',
- 'debug',
- 'views/viewfinder',
- 'views/focus',
- 'views/faces',
- 'lib/bind-all'
-], function (require, exports, module) {
- 'use strict';
- var cameraCoordinates = require('lib/camera-coordinates');
- var debug = require('debug')('controller:viewfinder');
- var ViewfinderView = require('views/viewfinder');
- var FocusView = require('views/focus');
- var FacesView = require('views/faces');
- var bindAll = require('lib/bind-all');
- module.exports = function (app) {
- return new ViewfinderController(app);
- };
- module.exports.ViewfinderController = ViewfinderController;
- function ViewfinderController(app) {
- bindAll(this);
- this.app = app;
- this.camera = app.camera;
- this.activity = app.activity;
- this.settings = app.settings;
- this.createViews();
- this.bindEvents();
- this.configure();
- debug('initialized');
- }
- ViewfinderController.prototype.createViews = function () {
- this.views = {};
- this.views.viewfinder = this.app.views.viewfinder || new ViewfinderView();
- this.views.focus = this.app.views.focus || new FocusView();
- this.views.faces = this.app.views.faces || new FacesView();
- this.views.focus.appendTo(this.views.viewfinder.el);
- this.views.faces.appendTo(this.views.viewfinder.el);
- this.views.viewfinder.appendTo(this.app.el);
- };
- ViewfinderController.prototype.configure = function () {
- var settings = this.app.settings;
- var zoomSensitivity = settings.viewfinder.get('zoomGestureSensitivity');
- this.sensitivity = zoomSensitivity * window.innerWidth;
- this.configureScaleType();
- this.configureGrid();
- };
- ViewfinderController.prototype.configureScaleType = function () {
- var scaleType = this.app.settings.viewfinder.get('scaleType');
- this.views.viewfinder.scaleType = scaleType;
- debug('set scale type: %s', scaleType);
- };
- ViewfinderController.prototype.configureGrid = function () {
- var grid = this.app.settings.grid.selected('key');
- this.views.viewfinder.set('grid', grid);
- };
- ViewfinderController.prototype.hideGrid = function () {
- 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'));
- this.views.viewfinder.on('click', this.onViewfinderClicked);
- this.camera.on('zoomconfigured', this.onZoomConfigured);
- this.camera.on('zoomchanged', this.onZoomChanged);
- this.camera.on('preview:started', this.show);
- this.app.on('camera:autofocuschanged', this.views.focus.showAutoFocusRing);
- this.app.on('camera:focusstatechanged', this.views.focus.setFocusState);
- this.app.on('camera:focusconfigured', this.onFocusConfigured);
- this.app.on('camera:shutter', this.views.viewfinder.shutter);
- this.app.on('camera:facesdetected', this.onFacesDetected);
- this.app.on('camera:configured', this.onCameraConfigured);
- this.app.on('camera:previewactive', this.onPreviewActive);
- this.app.on('busy', this.views.viewfinder.disable);
- this.app.on('ready', this.views.viewfinder.enable);
- this.app.on('camera:willchange', this.hide);
- this.app.on('previewgallery:opened', this.onGalleryOpened);
- this.app.on('previewgallery:closed', this.onGalleryClosed);
- this.app.on('settings:closed', this.onSettingsClosed);
- this.app.on('settings:opened', this.onSettingsOpened);
- this.app.settings.grid.on('change:selected', this.views.viewfinder.setter('grid'));
- this.app.on('battery:powersave', this.onPowerSave);
- this.app.on('pinch:changed', this.onPinchChanged);
- this.app.on('hidden', this.stopStream);
- };
- ViewfinderController.prototype.onCameraConfigured = function () {
- debug('configuring');
- this.loadStream();
- this.configurePreview();
- };
- ViewfinderController.prototype.show = function () {
- debug('show');
- if (!this.app.criticalPathDone) {
- this.views.viewfinder.fadeIn(true);
- return;
- }
- clearTimeout(this.showTimeout);
- this.showTimeout = setTimeout(this.views.viewfinder.fadeIn, 280);
- debug('schedule delayed fade-in');
- };
- ViewfinderController.prototype.hide = function () {
- debug('hide');
- clearTimeout(this.showTimeout);
- this.views.viewfinder.fadeOut();
- };
- ViewfinderController.prototype.onFocusConfigured = function (config) {
- this.views.focus.setFocusMode(config.mode);
- this.touchFocusEnabled = config.touchFocus;
- this.views.faces.clear();
- };
- ViewfinderController.prototype.onFacesDetected = function (faces) {
- var self = this;
- var faceCircles = [];
- var viewfinderSize = this.views.viewfinder.getSize();
- var viewportHeight = viewfinderSize.height;
- var viewportWidth = viewfinderSize.width;
- var sensorAngle = this.camera.getSensorAngle();
- var camera = this.app.settings.cameras.selected('key');
- var isFrontCamera = camera === 'front';
- faces.forEach(function (face, index) {
- var faceInPixels = cameraCoordinates.faceToPixels(face.bounds, viewportWidth, viewportHeight, sensorAngle, isFrontCamera);
- var faceCircle = self.calculateFaceCircle(faceInPixels);
- faceCircles.push(faceCircle);
- });
- this.views.faces.show();
- this.views.faces.render(faceCircles);
- };
- ViewfinderController.prototype.calculateFaceCircle = function (face) {
- var diameter = Math.max(face.width, face.height);
- var radius = diameter / 2;
- return {
- x: Math.round(face.left + face.width / 2 - radius),
- y: Math.round(face.top + face.height / 2 - radius),
- diameter: diameter
- };
- };
- ViewfinderController.prototype.loadStream = function () {
- this.camera.loadStreamInto(this.views.viewfinder.els.video);
- debug('stream started');
- };
- ViewfinderController.prototype.stopStream = function () {
- this.views.viewfinder.stopStream();
- debug('stream stopped');
- };
- ViewfinderController.prototype.onPowerSave = function (powerSave) {
- this.views.viewfinder.els.video.mozUseScreenWakeLock = !powerSave;
- };
- ViewfinderController.prototype.configurePreview = function () {
- var camera = this.app.settings.cameras.selected('key');
- var isFrontCamera = camera === 'front';
- var sensorAngle = this.camera.getSensorAngle();
- var previewSize = this.camera.previewSize();
- this.views.viewfinder.updatePreview(previewSize, sensorAngle, isFrontCamera);
- };
- ViewfinderController.prototype.onZoomConfigured = function () {
- var zoomSupported = this.camera.isZoomSupported();
- var zoomEnabled = this.app.settings.zoom.enabled();
- var enableZoom = zoomSupported && zoomEnabled;
- if (!enableZoom) {
- this.views.viewfinder.disableZoom();
- return;
- }
- if (this.app.settings.zoom.get('useZoomPreviewAdjustment')) {
- this.views.viewfinder.enableZoomPreviewAdjustment();
- } else {
- this.views.viewfinder.disableZoomPreviewAdjustment();
- }
- var minimumZoom = this.camera.getMinimumZoom();
- var maximumZoom = this.camera.getMaximumZoom();
- this.views.viewfinder.enableZoom(minimumZoom, maximumZoom);
- };
- ViewfinderController.prototype.onPinchChanged = function (deltaPinch) {
- var zoom = this.views.viewfinder._zoom * (1 + deltaPinch / this.sensitivity);
- this.views.viewfinder.setZoom(zoom);
- this.camera.setZoom(zoom);
- };
- ViewfinderController.prototype.onZoomChanged = function (zoom) {
- var zoomPreviewAdjustment = this.camera.getZoomPreviewAdjustment();
- this.views.viewfinder.setZoomPreviewAdjustment(zoomPreviewAdjustment);
- this.views.viewfinder.setZoom(zoom);
- };
- ViewfinderController.prototype.onViewfinderClicked = function (e) {
- if (!this.touchFocusEnabled) {
- return;
- }
- this.views.faces.hide();
- this.changeFocusPoint(e.pageX, e.pageY);
- };
- ViewfinderController.prototype.changeFocusPoint = function (x, y) {
- var viewfinderSize = this.views.viewfinder.getSize();
- var viewportHeight = viewfinderSize.height;
- var viewportWidth = viewfinderSize.width;
- var sensorAngle = this.camera.getSensorAngle();
- var focusAreaSize = 10;
- var focusAreaHalfSide = Math.round(focusAreaSize / 2);
- var focusAreaPixels = {
- left: x - focusAreaHalfSide,
- top: y - focusAreaHalfSide,
- right: x + focusAreaHalfSide,
- bottom: y + focusAreaHalfSide,
- width: focusAreaSize,
- height: focusAreaSize
- };
- var camera = this.app.settings.cameras.selected('key');
- var isFrontCamera = camera === 'front';
- var focusArea = cameraCoordinates.faceToCamera(focusAreaPixels, viewportWidth, viewportHeight, sensorAngle, isFrontCamera);
- var focusPoint = {
- x: x,
- y: y,
- area: focusArea
- };
- this.views.focus.setPosition(x, y);
- this.app.emit('viewfinder:focuspointchanged', focusPoint);
- };
- ViewfinderController.prototype.onSettingsOpened = function () {
- this.hideGrid();
- this.views.viewfinder.set('ariaHidden', true);
- };
- ViewfinderController.prototype.onSettingsClosed = function () {
- this.configureGrid();
- this.views.viewfinder.set('ariaHidden', false);
- };
- ViewfinderController.prototype.onGalleryOpened = function () {
- this.views.viewfinder.disable();
- this.views.viewfinder.set('ariaHidden', true);
- };
- ViewfinderController.prototype.onGalleryClosed = function () {
- this.views.viewfinder.enable();
- this.views.viewfinder.set('ariaHidden', false);
- };
- ViewfinderController.prototype.onPreviewActive = function (active) {
- if (!active) {
- this.stopStream();
- }
- };
+define("controllers/viewfinder",["require", "exports", "module", "lib/camera-coordinates", "debug", "views/viewfinder", "views/focus", "views/faces", "lib/bind-all"],function(require, exports, module) {
+'use strict';
+
+/**
+ * Dependencies
+ */
+
+var cameraCoordinates = require('lib/camera-coordinates');
+var debug = require('debug')('controller:viewfinder');
+var ViewfinderView = require('views/viewfinder');
+var FocusView = require('views/focus');
+var FacesView = require('views/faces');
+var bindAll = require('lib/bind-all');
+
+/**
+ * Exports
+ */
+
+module.exports = function(app) { return new ViewfinderController(app); };
+module.exports.ViewfinderController = ViewfinderController;
+
+/**
+ * Initialize a new `ViewfinderController`
+ *
+ * @param {App} app
+ */
+function ViewfinderController(app) {
+ bindAll(this);
+ this.app = app;
+ this.camera = app.camera;
+ this.activity = app.activity;
+ this.settings = app.settings;
+ this.createViews();
+ this.bindEvents();
+ this.configure();
+ debug('initialized');
+}
+
+/**
+ * Create and inject the views.
+ *
+ * @private
+ */
+ViewfinderController.prototype.createViews = function() {
+ this.views = {};
+ this.views.viewfinder = this.app.views.viewfinder || new ViewfinderView();
+ this.views.focus = this.app.views.focus || new FocusView();
+ this.views.faces = this.app.views.faces || new FacesView();
+ this.views.focus.appendTo(this.views.viewfinder.el);
+ this.views.faces.appendTo(this.views.viewfinder.el);
+ this.views.viewfinder.appendTo(this.app.el);
+};
+
+/**
+ * Initial configuration.
+ *
+ * @private
+ */
+ViewfinderController.prototype.configure = function() {
+ var settings = this.app.settings;
+ var zoomSensitivity = settings.viewfinder.get('zoomGestureSensitivity');
+ this.sensitivity = zoomSensitivity * window.innerWidth;
+ this.configureScaleType();
+ this.configureGrid();
+};
+
+/**
+ * Configure the viewfinder scale type,
+ * aspect fill/fit, depending on setting.
+ *
+ * @private
+ */
+ViewfinderController.prototype.configureScaleType = function() {
+ var scaleType = this.app.settings.viewfinder.get('scaleType');
+ this.views.viewfinder.scaleType = scaleType;
+ debug('set scale type: %s', scaleType);
+};
+
+/**
+ * Show/hide grid depending on currently
+ * selected option.
+ *
+ * @private
+ */
+ViewfinderController.prototype.configureGrid = function() {
+ var grid = this.app.settings.grid.selected('key');
+ this.views.viewfinder.set('grid', grid);
+};
+
+/**
+ * Hides the viewfinder frame-grid.
+ *
+ * @private
+ */
+ViewfinderController.prototype.hideGrid = function() {
+ this.views.viewfinder.set('grid', 'off');
+};
+
+/**
+ * Bind to relavant events.
+ *
+ * @private
+ */
+ViewfinderController.prototype.bindEvents = function() {
+ window.addEventListener('resize', this.configurePreview);
+
+ // View
+ 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'));
+ this.views.viewfinder.on('click', this.onViewfinderClicked);
+
+ // Tut tut, we shouldn't have direct coupling here.
+ // TODO: Camera events should be relayed through the app.
+ this.camera.on('zoomconfigured', this.onZoomConfigured);
+ this.camera.on('zoomchanged', this.onZoomChanged);
+ this.camera.on('preview:started', this.show);
+
+ // Camera
+ this.app.on('camera:autofocuschanged', this.views.focus.showAutoFocusRing);
+ this.app.on('camera:focusstatechanged', this.views.focus.setFocusState);
+ this.app.on('camera:focusconfigured', this.onFocusConfigured);
+ this.app.on('camera:shutter', this.views.viewfinder.shutter);
+ this.app.on('camera:facesdetected', this.onFacesDetected);
+ this.app.on('camera:configured', this.onCameraConfigured);
+ this.app.on('camera:previewactive', this.onPreviewActive);
+ this.app.on('busy', this.views.viewfinder.disable);
+ this.app.on('ready', this.views.viewfinder.enable);
+ this.app.on('camera:willchange', this.hide);
+
+ // Preview Gallery
+ this.app.on('previewgallery:opened', this.onGalleryOpened);
+ this.app.on('previewgallery:closed', this.onGalleryClosed);
+
+ // Settings
+ this.app.on('settings:closed', this.onSettingsClosed);
+ this.app.on('settings:opened', this.onSettingsOpened);
+ this.app.settings.grid.on('change:selected',
+ this.views.viewfinder.setter('grid'));
+
+ // Battery
+ this.app.on('battery:powersave', this.onPowerSave);
+
+ // App
+ this.app.on('pinch:changed', this.onPinchChanged);
+ this.app.on('hidden', this.stopStream);
+};
+
+/**
+ * Perform required viewfinder configuration
+ * once the camera has configured.
+ *
+ * @private
+ */
+ViewfinderController.prototype.onCameraConfigured = function() {
+ debug('configuring');
+ this.loadStream();
+ this.configurePreview();
+};
+
+/**
+ * Show the viewfinder.
+ *
+ * If the critical-path is not done we
+ * fade the viewfinder in straight away
+ * to make sure we have the quickest
+ * startup possible.
+ *
+ * We have to use a timeout for all other
+ * viewfinder showing actions conceal a Gecko
+ * rendering bug whereby the video element has
+ * not yet 'visually' switched to the new stream
+ * when we get the preview 'started' event from
+ * the camera. This means the user sees a flicker
+ * if we don't give it some time to adjust.
+ *
+ * We clear these timeouts to avoid multiple pending
+ * timeouts, which could cause pain.
+ *
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=982230
+ *
+ * @private
+ */
+ViewfinderController.prototype.show = function() {
+ debug('show');
+ if (!this.app.criticalPathDone) {
+ this.views.viewfinder.fadeIn(true);
+ return;
+ }
+
+ clearTimeout(this.showTimeout);
+ this.showTimeout = setTimeout(this.views.viewfinder.fadeIn, 280);
+ debug('schedule delayed fade-in');
+};
+
+/**
+ * Fades the viewfinder in.
+ *
+ * We clear any pending timeouts here
+ * to prevent unusual behaviour ensuing.
+ *
+ * @private
+ */
+ViewfinderController.prototype.hide = function() {
+ debug('hide');
+ clearTimeout(this.showTimeout);
+ this.views.viewfinder.fadeOut();
+};
+
+/**
+ * Sets appropiate flags when the camera focus is configured
+ */
+ViewfinderController.prototype.onFocusConfigured = function(config) {
+ this.views.focus.setFocusMode(config.mode);
+ this.touchFocusEnabled = config.touchFocus;
+ this.views.faces.clear();
+};
+
+ViewfinderController.prototype.onFacesDetected = function(faces) {
+ var self = this;
+ var faceCircles = [];
+ var viewfinderSize = this.views.viewfinder.getSize();
+ var viewportHeight = viewfinderSize.height;
+ var viewportWidth = viewfinderSize.width;
+ var sensorAngle = this.camera.getSensorAngle();
+ var camera = this.app.settings.cameras.selected('key');
+ var isFrontCamera = camera === 'front';
+
+ faces.forEach(function(face, index) {
+ // Face comes in camera coordinates from gecko
+ var faceInPixels = cameraCoordinates.faceToPixels(
+ face.bounds, viewportWidth, viewportHeight, sensorAngle, isFrontCamera);
+ var faceCircle = self.calculateFaceCircle(faceInPixels);
+ faceCircles.push(faceCircle);
+ });
+ this.views.faces.show();
+ this.views.faces.render(faceCircles);
+};
+
+ViewfinderController.prototype.calculateFaceCircle = function(face) {
+ var diameter = Math.max(face.width, face.height);
+ var radius = diameter / 2;
+ return {
+ x: Math.round(face.left + face.width / 2 - radius),
+ y: Math.round(face.top + face.height / 2 - radius),
+ diameter: diameter
+ };
+};
+
+/**
+ * Start the viewfinder stream flowing
+ * with the current camera configuration.
+ *
+ * This indirectly enforces a screen wakelock,
+ * meaning the device is unable to go to sleep.
+ *
+ * We don't want the stream to start flowing if
+ * the preview-gallery is open, as this prevents
+ * the device falling asleep.
+ *
+ * @private
+ */
+ViewfinderController.prototype.loadStream = function() {
+ this.camera.loadStreamInto(this.views.viewfinder.els.video);
+ debug('stream started');
+};
+
+/**
+ * Stop the preview stream flowing.
+ *
+ * This indirectly removes the wakelock
+ * that is magically enforced by the
+ * flowing camera stream. Meaning the
+ * device is able to go to sleep.
+ *
+ * @private
+ */
+ViewfinderController.prototype.stopStream = function() {
+ this.views.viewfinder.stopStream();
+ debug('stream stopped');
+};
+
+/**
+ * Power save state changed. If entering power
+ * save mode, release the screen wake lock to
+ * allow it to timeout without user interaction,
+ * otherwise acquire the wake lock.
+ *
+ * @private
+ */
+ViewfinderController.prototype.onPowerSave = function(powerSave) {
+ this.views.viewfinder.els.video.mozUseScreenWakeLock = !powerSave;
+};
+
+/**
+ * Configure the size and postion
+ * of the preview video stream.
+ *
+ * @private
+ */
+ViewfinderController.prototype.configurePreview = function() {
+ var camera = this.app.settings.cameras.selected('key');
+ var isFrontCamera = camera === 'front';
+ var sensorAngle = this.camera.getSensorAngle();
+ var previewSize = this.camera.previewSize();
+
+ this.views.viewfinder.updatePreview(previewSize, sensorAngle, isFrontCamera);
+};
+
+/**
+ * Configures the viewfinder
+ * to the current camera.
+ *
+ * @private
+ */
+ViewfinderController.prototype.onZoomConfigured = function() {
+ var zoomSupported = this.camera.isZoomSupported();
+ var zoomEnabled = this.app.settings.zoom.enabled();
+ var enableZoom = zoomSupported && zoomEnabled;
+
+ if (!enableZoom) {
+ this.views.viewfinder.disableZoom();
+ return;
+ }
+
+ if (this.app.settings.zoom.get('useZoomPreviewAdjustment')) {
+ this.views.viewfinder.enableZoomPreviewAdjustment();
+ } else {
+ this.views.viewfinder.disableZoomPreviewAdjustment();
+ }
+
+ var minimumZoom = this.camera.getMinimumZoom();
+ var maximumZoom = this.camera.getMaximumZoom();
+
+ this.views.viewfinder.enableZoom(minimumZoom, maximumZoom);
+};
+
+/**
+ * Updates the zoom level on the camera
+ * when the pinch changes.
+ *
+ * @private
+ */
+ViewfinderController.prototype.onPinchChanged = function(deltaPinch) {
+ var zoom = this.views.viewfinder._zoom *
+ (1 + (deltaPinch / this.sensitivity));
+ this.views.viewfinder.setZoom(zoom);
+ this.camera.setZoom(zoom);
+};
+
+/**
+ * Responds to changes of the `zoom` value on the Camera to update the
+ * view's internal state so that the pinch-to-zoom gesture can resume
+ * zooming from the updated value. Also, updates the CSS scale transform
+ * on the tag to compensate for zooming beyond the
+ * `maxHardwareZoom` value.
+ *
+ * @param {Number} zoom
+ */
+ViewfinderController.prototype.onZoomChanged = function(zoom) {
+ var zoomPreviewAdjustment = this.camera.getZoomPreviewAdjustment();
+ this.views.viewfinder.setZoomPreviewAdjustment(zoomPreviewAdjustment);
+ this.views.viewfinder.setZoom(zoom);
+};
+
+ViewfinderController.prototype.onViewfinderClicked = function(e) {
+ if (!this.touchFocusEnabled) { return; }
+ this.views.faces.hide();
+ this.changeFocusPoint(e.pageX, e.pageY);
+};
+
+ViewfinderController.prototype.changeFocusPoint = function(x, y) {
+ var viewfinderSize = this.views.viewfinder.getSize();
+ var viewportHeight = viewfinderSize.height;
+ var viewportWidth = viewfinderSize.width;
+ var sensorAngle = this.camera.getSensorAngle();
+ var focusAreaSize = 10;
+ var focusAreaHalfSide = Math.round(focusAreaSize / 2);
+ // Top Left corner of the area and its size
+ var focusAreaPixels = {
+ left: x - focusAreaHalfSide,
+ top: y - focusAreaHalfSide,
+ right: x + focusAreaHalfSide,
+ bottom: y + focusAreaHalfSide,
+ width: focusAreaSize,
+ height: focusAreaSize
+ };
+ var camera = this.app.settings.cameras.selected('key');
+ var isFrontCamera = camera === 'front';
+ var focusArea = cameraCoordinates.faceToCamera(
+ focusAreaPixels, viewportWidth, viewportHeight, sensorAngle, isFrontCamera);
+ var focusPoint = {
+ x: x,
+ y: y,
+ area: focusArea
+ };
+ this.views.focus.setPosition(x, y);
+ this.app.emit('viewfinder:focuspointchanged', focusPoint);
+};
+
+ViewfinderController.prototype.onSettingsOpened = function() {
+ this.hideGrid();
+ // Make viewfinder invisible to the screen reader since it is behind settings
+ // overlay.
+ this.views.viewfinder.set('ariaHidden', true);
+};
+
+ViewfinderController.prototype.onSettingsClosed = function() {
+ this.configureGrid();
+ // Make viewfinder visible to the screen reader again when settings are
+ // closed.
+ this.views.viewfinder.set('ariaHidden', false);
+};
+
+/**
+ * Disables the viewfinder stream
+ * and pinch events.
+ *
+ * @private
+ */
+ViewfinderController.prototype.onGalleryOpened = function() {
+ this.views.viewfinder.disable();
+ // Make viewfinder invisible to the screen reader since it is behind gallery
+ // overlay.
+ this.views.viewfinder.set('ariaHidden', true);
+};
+
+/**
+ * Enables the viewfinder stream
+ * and pinch events.
+ *
+ * @private
+ */
+ViewfinderController.prototype.onGalleryClosed = function() {
+ this.views.viewfinder.enable();
+ // Make viewfinder visible to the screen reader again when gallery is closed.
+ this.views.viewfinder.set('ariaHidden', false);
+};
+
+ViewfinderController.prototype.onPreviewActive = function(active) {
+ if (!active) {
+ this.stopStream();
+ }
+};
+
});
define('lib/get-aspect', [
'require',
@@ -6001,279 +7365,490 @@ define('lib/format-picture-sizes', [
return Math.round(w * h / 1000000);
}
});
-define('controllers/settings', [
- 'require',
- 'exports',
- 'module',
- 'lib/format-recorder-profiles',
- 'lib/format-picture-sizes',
- 'debug',
- 'lib/bind-all'
-], function (require, exports, module) {
- 'use strict';
- var formatRecorderProfiles = require('lib/format-recorder-profiles');
- var formatPictureSizes = require('lib/format-picture-sizes');
- var debug = require('debug')('controller:settings');
- var bindAll = require('lib/bind-all');
- module.exports = function (app) {
- return new SettingsController(app);
- };
- module.exports.SettingsController = SettingsController;
- function SettingsController(app) {
- bindAll(this);
- this.app = app;
- this.require = app.require;
- this.settings = app.settings;
- this.activity = app.activity;
- this.notification = app.views.notification;
- this.nav = app.nav || navigator;
- this.SettingsView = app.SettingsView;
- this.formatPictureSizes = app.formatPictureSizes || formatPictureSizes;
- this.formatRecorderProfiles = app.formatRecorderProfiles || formatRecorderProfiles;
- this.configure();
- this.bindEvents();
- debug('initialized');
+define("controllers/settings",["require", "exports", "module", "lib/format-recorder-profiles", "lib/format-picture-sizes", "debug", "lib/bind-all"],function(require, exports, module) {
+'use strict';
+
+/**
+ * Dependencies
+ */
+
+var formatRecorderProfiles = require('lib/format-recorder-profiles');
+var formatPictureSizes = require('lib/format-picture-sizes');
+var debug = require('debug')('controller:settings');
+var bindAll = require('lib/bind-all');
+
+/**
+ * Exports
+ */
+
+module.exports = function(app) { return new SettingsController(app); };
+module.exports.SettingsController = SettingsController;
+
+/**
+ * Initialize a new `SettingsController`
+ *
+ * @constructor
+ * @param {App} app
+ */
+function SettingsController(app) {
+ bindAll(this);
+ this.app = app;
+ this.require = app.require;
+ this.settings = app.settings;
+ this.activity = app.activity;
+ this.notification = app.views.notification;
+
+ // Provide test hooks
+ this.nav = app.nav || navigator;
+ this.SettingsView = app.SettingsView;
+ this.formatPictureSizes = app.formatPictureSizes || formatPictureSizes;
+ this.formatRecorderProfiles = app.formatRecorderProfiles ||
+ formatRecorderProfiles;
+
+ this.configure();
+ this.bindEvents();
+ debug('initialized');
+}
+
+/**
+ * Registers settings 'aliases' that provide
+ * a single interface for settings such as
+ * `flashModes` where we have `flashModesPicture`
+ * and `flashModesVideo`.
+ *
+ * This means that we can just use settings.flashModes,
+ * and be confident that it will interface with the
+ * correct setting depending on the value of `mode`.
+ *
+ * You can always use the underlying settings
+ * directly if you need that kind of control.
+ */
+SettingsController.prototype.configure = function() {
+ this.setupRecorderProfilesAlias();
+ this.setupPictureSizesAlias();
+ this.setupFlashModesAlias();
+};
+
+/**
+ * Creates a SettingAlias that dynamically
+ * interfaces with the correct recorder
+ * profile Setting based on which camera
+ * ('front'/'back') is selected.
+ *
+ * @private
+ */
+SettingsController.prototype.setupRecorderProfilesAlias = function() {
+ var settings = this.settings;
+ this.settings.alias({
+ key: 'recorderProfiles',
+ settings: {
+ back: this.settings.recorderProfilesBack,
+ front: this.settings.recorderProfilesFront
+ },
+ get: function() {
+ var camera = settings.cameras.selected('key');
+ return this.settings[camera];
}
- SettingsController.prototype.configure = function () {
- this.setupRecorderProfilesAlias();
- this.setupPictureSizesAlias();
- this.setupFlashModesAlias();
- };
- SettingsController.prototype.setupRecorderProfilesAlias = function () {
- var settings = this.settings;
- this.settings.alias({
- key: 'recorderProfiles',
- settings: {
- back: this.settings.recorderProfilesBack,
- front: this.settings.recorderProfilesFront
- },
- get: function () {
- var camera = settings.cameras.selected('key');
- return this.settings[camera];
- }
+ });
+};
+
+/**
+ * Creates a SettingAlias that dynamically
+ * interfaces with the correct picture
+ * size Setting based on which camera
+ * ('front'/'back') is selected.
+ *
+ * @private
+ */
+SettingsController.prototype.setupPictureSizesAlias = function() {
+ var settings = this.settings;
+ this.settings.alias({
+ key: 'pictureSizes',
+ settings: {
+ back: this.settings.pictureSizesBack,
+ front: this.settings.pictureSizesFront
+ },
+ get: function() {
+ var camera = settings.cameras.selected('key');
+ return this.settings[camera];
+ }
+ });
+};
+
+/**
+ * Creates a SettingAlias that dynamically
+ * interfaces with the correct flash-modes
+ * Setting based on which mode ('picture'/
+ * 'video') is selected.
+ *
+ * @private
+ */
+SettingsController.prototype.setupFlashModesAlias = function() {
+ var settings = this.settings;
+ this.settings.alias({
+ key: 'flashModes',
+ settings: {
+ picture: this.settings.flashModesPicture,
+ video: this.settings.flashModesVideo
+ },
+ get: function() {
+ var mode = settings.mode.selected('key');
+ return this.settings[mode];
+ }
+ });
+};
+
+/**
+ * Bind to app events.
+ *
+ * @private
+ */
+SettingsController.prototype.bindEvents = function() {
+ this.app.on('localized', this.formatPictureSizeTitles);
+ this.app.on('settings:toggle', this.toggleSettings);
+ this.app.on('camera:newcamera', this.onNewCamera);
+ this.app.on('activity:pick', this.onPickActivity);
+};
+
+/**
+ * Toggle the settings menu open/closed.
+ *
+ * @private
+ */
+SettingsController.prototype.toggleSettings = function() {
+ if (this.view) { this.closeSettings(); }
+ else { this.openSettings(); }
+};
+
+/**
+ * Render and display the settings menu.
+ *
+ * We use settings.menu() to retrieve
+ * and ordered list of settings that
+ * have a `menu` property.
+ *
+ * @private
+ */
+SettingsController.prototype.openSettings = function() {
+ debug('open settings');
+ var self = this;
+ this.app.emit('busy', 'lazyLoading');
+ this.require(['views/settings'], function(SettingsView) {
+ self.app.emit('ready');
+
+ if (!self.SettingsView) {
+ self.SettingsView = SettingsView;
+ }
+ if (self.view) { return; }
+
+ var items = self.menuItems();
+ self.view = new self.SettingsView({ items: items })
+ .render()
+ .appendTo(self.app.el)
+ .on('click:close', self.closeSettings)
+ .on('click:option', self.onOptionTap);
+
+ // Make sure the view is
+ // hidden before fading in
+ self.view.hide();
+ self.view.fadeIn();
+
+ self.app.emit('settings:opened');
+ debug('settings opened');
+ });
+};
+
+/**
+ * Destroy the settings menu.
+ *
+ * @private
+ */
+SettingsController.prototype.closeSettings = function(done) {
+ debug('close settings');
+ if (!this.view) { return; }
+ var self = this;
+ this.view.fadeOut(function() {
+ self.view.destroy();
+ self.view = null;
+ self.app.emit('settings:closed');
+ debug('settings closed');
+ if (typeof done === 'function') { done(); }
+ });
+};
+
+/**
+ * Selects the option that was
+ * clicked on the setting.
+ *
+ * @param {String} key
+ * @param {Setting} setting
+ * @private
+ */
+SettingsController.prototype.onOptionTap = function(key, setting) {
+ var flashMode = this.settings.flashModesPicture.selected('key');
+ var ishdrOn = setting.key === 'hdr' && key === 'on';
+ var flashDeactivated = flashMode !== 'off' && ishdrOn;
+ var self = this;
+
+ self.closeSettings(function() {
+ setting.select(key);
+ self.notify(setting, flashDeactivated);
+ });
+};
+
+/**
+ * Adjusts settings to meet requirements
+ * on new pick activity.
+ *
+ * @param {Object} data
+ * @private
+ */
+SettingsController.prototype.onPickActivity = function(data) {
+ debug('pick activity', data);
+
+ var setting;
+ var options;
+ var updated = false;
+ var maxFileSize = data.maxFileSizeBytes;
+ var maxPixelSize = data.maxPixelSize;
+
+ // Settings changes made in 'pick'
+ // sessions shouldn't persist.
+ this.settings.dontSave();
+
+ if (maxPixelSize) {
+ setting = this.settings.pictureSizes;
+ var lastMaxPixelSize = setting.get('maxPixelSize');
+
+ this.settings.pictureSizesFront.set('maxPixelSize', maxPixelSize);
+ this.settings.pictureSizesBack.set('maxPixelSize', maxPixelSize);
+ debug('set maxPixelSize: %s', maxPixelSize);
+
+ if (lastMaxPixelSize !== maxPixelSize) {
+ options = setting.get('options');
+ var restricted = [];
+ if (options && options.length > 0) {
+ options.forEach(function(option) {
+ if (option.pixelSize <= maxPixelSize) {
+ restricted.push(option);
+ }
});
- };
- SettingsController.prototype.setupPictureSizesAlias = function () {
- var settings = this.settings;
- this.settings.alias({
- key: 'pictureSizes',
- settings: {
- back: this.settings.pictureSizesBack,
- front: this.settings.pictureSizesFront
- },
- get: function () {
- var camera = settings.cameras.selected('key');
- return this.settings[camera];
- }
- });
- };
- SettingsController.prototype.setupFlashModesAlias = function () {
- var settings = this.settings;
- this.settings.alias({
- key: 'flashModes',
- settings: {
- picture: this.settings.flashModesPicture,
- video: this.settings.flashModesVideo
- },
- get: function () {
- var mode = settings.mode.selected('key');
- return this.settings[mode];
- }
- });
- };
- SettingsController.prototype.bindEvents = function () {
- this.app.on('localized', this.formatPictureSizeTitles);
- this.app.on('settings:toggle', this.toggleSettings);
- this.app.on('camera:newcamera', this.onNewCamera);
- this.app.on('activity:pick', this.onPickActivity);
- };
- SettingsController.prototype.toggleSettings = function () {
- if (this.view) {
- this.closeSettings();
- } else {
- this.openSettings();
- }
- };
- SettingsController.prototype.openSettings = function () {
- debug('open settings');
- var self = this;
- this.app.emit('busy', 'lazyLoading');
- this.require(['views/settings'], function (SettingsView) {
- self.app.emit('ready');
- if (!self.SettingsView) {
- self.SettingsView = SettingsView;
- }
- if (self.view) {
- return;
- }
- var items = self.menuItems();
- self.view = new self.SettingsView({ items: items }).render().appendTo(self.app.el).on('click:close', self.closeSettings).on('click:option', self.onOptionTap);
- self.view.hide();
- self.view.fadeIn();
- self.app.emit('settings:opened');
- debug('settings opened');
- });
- };
- SettingsController.prototype.closeSettings = function (done) {
- debug('close settings');
- if (!this.view) {
- return;
- }
- var self = this;
- this.view.fadeOut(function () {
- self.view.destroy();
- self.view = null;
- self.app.emit('settings:closed');
- debug('settings closed');
- if (typeof done === 'function') {
- done();
- }
- });
- };
- SettingsController.prototype.onOptionTap = function (key, setting) {
- var flashMode = this.settings.flashModesPicture.selected('key');
- var ishdrOn = setting.key === 'hdr' && key === 'on';
- var flashDeactivated = flashMode !== 'off' && ishdrOn;
- var self = this;
- self.closeSettings(function () {
- setting.select(key);
- self.notify(setting, flashDeactivated);
- });
- };
- SettingsController.prototype.onPickActivity = function (data) {
- debug('pick activity', data);
- var setting;
- var options;
- var updated = false;
- var maxFileSize = data.maxFileSizeBytes;
- var maxPixelSize = data.maxPixelSize;
- this.settings.dontSave();
- if (maxPixelSize) {
- setting = this.settings.pictureSizes;
- var lastMaxPixelSize = setting.get('maxPixelSize');
- this.settings.pictureSizesFront.set('maxPixelSize', maxPixelSize);
- this.settings.pictureSizesBack.set('maxPixelSize', maxPixelSize);
- debug('set maxPixelSize: %s', maxPixelSize);
- if (lastMaxPixelSize !== maxPixelSize) {
- options = setting.get('options');
- var restricted = [];
- if (options && options.length > 0) {
- options.forEach(function (option) {
- if (option.pixelSize <= maxPixelSize) {
- restricted.push(option);
- }
- });
- setting.resetOptions(restricted);
- updated = true;
- }
- }
- }
- if (maxFileSize) {
- setting = this.settings.recorderProfiles;
- var lastMaxFileSize = setting.get('maxFileSizeBytes');
- this.settings.recorderProfilesFront.set('maxFileSizeBytes', maxFileSize);
- this.settings.recorderProfilesBack.set('maxFileSizeBytes', maxFileSize);
- debug('set maxFileSize: %s', maxFileSize);
- if (lastMaxFileSize !== maxFileSize) {
- options = setting.get('options');
- if (options && options.length > 1) {
- setting.resetOptions([options[options.length - 1]]);
- updated = true;
- }
- }
- }
- if (updated) {
- this.app.emit('settings:configured');
- }
- };
- SettingsController.prototype.notify = function (setting, flashDeactivated) {
- var dontNotify = setting.get('notifications') === false;
- if (dontNotify) {
- return;
- }
- var localizable = setting.get('optionsLocalizable') !== false;
- var title = '';
- var html;
- var optionTitle = localizable ? '' : '' + setting.selected('title') + '';
- if (flashDeactivated) {
- html = title + ' ' + optionTitle + '
' + '';
- } else {
- html = title + '
' + optionTitle;
- }
- this.notification.display({ text: { html: html } });
- };
- SettingsController.prototype.onNewCamera = function (camera) {
- debug('new capabilities');
- var capabilities = camera.capabilities;
- this.settings.hdr.filterOptions(capabilities.hdr);
- this.settings.flashModesPicture.filterOptions(capabilities.flashModes);
- this.settings.flashModesVideo.filterOptions(capabilities.flashModes);
- this.configurePictureSizes(camera);
- this.configureRecorderProfiles(camera);
- this.app.emit('settings:configured');
- debug('settings configured to new capabilities');
- };
- SettingsController.prototype.configurePictureSizes = function (camera) {
- debug('configuring picture sizes');
- var maxPixelSize = window.CONFIG_MAX_IMAGE_PIXEL_SIZE;
- var sizes = camera.capabilities.pictureSizes;
- var setting = this.settings.pictureSizes;
- var currentSize = camera.pictureSize;
- var currentSizeKey = currentSize.width + 'x' + currentSize.height;
- var options = {
- exclude: setting.get('exclude'),
- include: setting.get('include'),
- maxPixelSize: maxPixelSize
- };
- var formatted = this.formatPictureSizes(sizes, options);
- // Desktop and Android sensors may only expose a size outside Gaia's 4:3 preset.
- if (!formatted.length && sizes.length) {
- formatted = this.formatPictureSizes(sizes, {});
- }
- setting.resetOptions(formatted);
- if (!setting.current().fetched) {
- setting.select(currentSizeKey, { silent: true });
- }
- this.formatPictureSizeTitles();
- debug('configured pictureSizes', setting.selected('key'));
- };
- SettingsController.prototype.configureRecorderProfiles = function (camera) {
- var sizes = camera.capabilities.recorderProfiles;
- var currentProfile = camera.recorderProfile;
- var setting = this.settings.recorderProfiles;
- var maxFileSize = setting.get('maxFileSizeBytes');
- var exclude = setting.get('exclude');
- var options = { exclude: exclude };
- var items = this.formatRecorderProfiles(sizes, options);
- if (maxFileSize) {
- items = [items[items.length - 1]];
- }
- setting.resetOptions(items);
- if (!maxFileSize && !setting.current().fetched) {
- setting.select(currentProfile, { silent: true });
- }
- };
- SettingsController.prototype.formatPictureSizeTitles = function () {
- return document.l10n.formatValue('mp').then(value => {
- var options = this.settings.pictureSizes.get('options');
- options.forEach(function (size) {
- var data = size.data;
- var mp = data.mp ? data.mp + value + ' ' : '';
- size.title = mp + size.key;
- });
- debug('picture size titles formatted');
- });
- };
- SettingsController.prototype.menuItems = function () {
- var items = this.settings.settingsMenu.get('items');
- return items.filter(this.validMenuItem, this).map(function (item) {
- return this.settings[item.key];
- }, this);
- };
- SettingsController.prototype.validMenuItem = function (item) {
- var setting = this.settings[item.key];
- return !!setting && setting.supported();
- };
+ setting.resetOptions(restricted);
+ updated = true;
+ }
+ }
+ }
+
+ if (maxFileSize) {
+ setting = this.settings.recorderProfiles;
+ var lastMaxFileSize = setting.get('maxFileSizeBytes');
+
+ this.settings.recorderProfilesFront.set('maxFileSizeBytes', maxFileSize);
+ this.settings.recorderProfilesBack.set('maxFileSizeBytes', maxFileSize);
+ debug('set maxFileSize: %s', maxFileSize);
+
+ if (lastMaxFileSize !== maxFileSize) {
+ options = setting.get('options');
+ if (options && options.length > 1) {
+ setting.resetOptions([options[options.length - 1]]);
+ updated = true;
+ }
+ }
+ }
+
+ // If the size restrictions come in after the camera was brought
+ // up, then we must retrigger a configuration event
+ if (updated) {
+ this.app.emit('settings:configured');
+ }
+};
+
+/**
+ * Display a notifcation showing the
+ * current state of the given setting.
+ *
+ * If `notification` is `false in config
+ * for a setting then we don't show one.
+ *
+ * @param {Setting} setting
+ * @private
+ */
+SettingsController.prototype.notify = function(setting, flashDeactivated) {
+ var dontNotify = setting.get('notifications') === false;
+ if (dontNotify) { return; }
+
+ var localizable = setting.get('optionsLocalizable') !== false;
+ var title = '';
+ var html;
+
+ // Localize option title only if not specified in the config
+ var optionTitle = localizable ?
+ '' :
+ '' + setting.selected('title') + '';
+
+ // Check if the `flashMode` setting is going to be deactivated as part
+ // of the change in the `hdr` setting and display a specialized
+ // notification if that is the case
+ if (flashDeactivated) {
+ html = title + ' ' + optionTitle + '
' +
+ '';
+ } else {
+ html = title + '
' + optionTitle;
+ }
+
+ this.notification.display({ text: { html: html }});
+};
+
+/**
+ * When the hardware capabilities change
+ * we update the available options for
+ * each setting to match what is available.
+ *
+ * The rest of the app should listen for
+ * the 'settings:configured' event as an
+ * indication to update UI etc.
+ *
+ * @param {Object} capabilities
+ */
+SettingsController.prototype.onNewCamera = function(camera) {
+ debug('new capabilities');
+ var capabilities = camera.capabilities;
+
+ this.settings.hdr.filterOptions(capabilities.hdr);
+ this.settings.flashModesPicture.filterOptions(capabilities.flashModes);
+ this.settings.flashModesVideo.filterOptions(capabilities.flashModes);
+
+ this.configurePictureSizes(camera);
+ this.configureRecorderProfiles(camera);
+
+ // Let the rest of the app know we're good to go.
+ this.app.emit('settings:configured');
+ debug('settings configured to new capabilities');
+};
+
+/**
+ * Formats the raw pictureSizes into
+ * a format that Setting class can
+ * work with, then resets the pictureSize
+ * options.
+ *
+ * @param {Array} sizes
+ */
+SettingsController.prototype.configurePictureSizes = function(camera) {
+ debug('configuring picture sizes');
+ var maxPixelSize = window.CONFIG_MAX_IMAGE_PIXEL_SIZE;
+ var sizes = camera.capabilities.pictureSizes;
+ var setting = this.settings.pictureSizes;
+ var currentSize = camera.pictureSize;
+ var currentSizeKey = currentSize.width + 'x' + currentSize.height;
+ var options = {
+ exclude: setting.get('exclude'),
+ include: setting.get('include'),
+ maxPixelSize: maxPixelSize
+ };
+
+ var formatted = this.formatPictureSizes(sizes, options);
+ // A sensor may expose only sizes outside Gaia's original 4:3 preset.
+ if (!formatted.length && sizes.length) {
+ formatted = this.formatPictureSizes(sizes, {});
+ }
+ setting.resetOptions(formatted);
+
+ // If the setting has no stored default
+ // set the setting to match the current
+ // mozCamera setting.
+ if (!setting.current().fetched) {
+ setting.select(currentSizeKey, { silent: true });
+ }
+
+ this.formatPictureSizeTitles();
+
+ debug('configured pictureSizes', setting.selected('key'));
+};
+
+/**
+ * Formats the raw recorderProfiles
+ * into a format that Setting class can
+ * work with, then resets the recorderProfile
+ * options.
+ *
+ * @param {Array} sizes
+ */
+SettingsController.prototype.configureRecorderProfiles = function(camera) {
+ var sizes = camera.capabilities.recorderProfiles;
+ var currentProfile = camera.recorderProfile;
+ var setting = this.settings.recorderProfiles;
+ var maxFileSize = setting.get('maxFileSizeBytes');
+ var exclude = setting.get('exclude');
+ var options = { exclude: exclude };
+ var items = this.formatRecorderProfiles(sizes, options);
+
+ // If a file size limit has been imposed,
+ // pick the lowest-res (last) profile only.
+ if (maxFileSize) { items = [items[items.length - 1]]; }
+
+ setting.resetOptions(items);
+
+ // Set the recorder profile to match
+ // the currently selected mozCamera profile
+ if (!maxFileSize && !setting.current().fetched) {
+ setting.select(currentProfile, { silent: true });
+ }
+};
+
+/**
+ * Creates a localized `title` property
+ * on each pictureSize option. This is
+ * used within the settings-menu.
+ *
+ * This is run each time `configurePictureSizes`
+ * is run and each time the app recieves a
+ * 'localized' event.
+ *
+ * If the app isn't 'localized' yet, we don't do
+ * anything and wait for the 'localized'
+ * event binding to run the function.
+ *
+ * @private
+ */
+SettingsController.prototype.formatPictureSizeTitles = function() {
+ return document.l10n.formatValue('mp').then((value) => {
+ var options = this.settings.pictureSizes.get('options');
+ options.forEach(function(size) {
+ var data = size.data;
+ var mp = data.mp ? data.mp + value + ' ' : '';
+ size.title = mp + size.key;
+ });
+
+ debug('picture size titles formatted');
+ });
+};
+
+/**
+ * Returns a list of settings
+ * based on the `settingsMenu`
+ * configuration.
+ *
+ * @return {Array}
+ */
+SettingsController.prototype.menuItems = function() {
+ var items = this.settings.settingsMenu.get('items');
+ return items.filter(this.validMenuItem, this)
+ .map(function(item) { return this.settings[item.key]; }, this);
+};
+
+/**
+ * Tests if the passed `settingsMenu`
+ * item is allowed in the settings menu.
+ *
+ * @param {Object} item
+ * @return {Boolean}
+ */
+SettingsController.prototype.validMenuItem = function(item) {
+ var setting = this.settings[item.key];
+ return !!setting && setting.supported();
+};
+
});
define('lib/bytes-to-pixels', [
'require',
diff --git a/overrides/marketplace.gaiamobile.org/marketplace.js b/overrides/marketplace.gaiamobile.org/marketplace.js
new file mode 100644
index 00000000..bb7eee4f
--- /dev/null
+++ b/overrides/marketplace.gaiamobile.org/marketplace.js
@@ -0,0 +1,42 @@
+(async()=>{
+ const frame=document.getElementById('catalogue'),message=document.getElementById('message'),dialog=document.getElementById('confirm');
+ let attempt=0,busy=false,prepared=null,installed=null,language=await VulpesCompat.call('locale.current');
+ const fr=()=>/^fr(?:-|$)/i.test(language);
+ const text=(f,e)=>fr()?f:e;
+ function labels(){document.getElementById('browse').textContent=text('Ouvrir le catalogue','Open catalogue');document.getElementById('open').textContent=text('Ouvrir','Open');document.getElementById('install').textContent=text('Installer','Install');document.getElementById('cancel').textContent=text('Annuler','Cancel');}
+ function loadFailed(){status(text('Catalogue inaccessible. Vérifiez la connexion Internet, puis réessayez.','Catalogue unavailable. Check your Internet connection, then try again.'));document.getElementById('open').hidden=true;}
+ frame.addEventListener('mozbrowsererror',loadFailed);
+ frame.addEventListener('mozbrowserclose',()=>{
+ if(busy)return;
+ status(text('Le catalogue est fermé. Vous pouvez le rouvrir ci-dessous.',
+ 'The catalogue is closed. You can reopen it below.'));
+ document.getElementById('open').hidden=true;
+ });
+ let catalogueUrl='';
+ function browse(reload=false){labels();message.hidden=true;dialog.hidden=true;frame.style.visibility='visible';const next='https://vulpes-os.org/marketplace/?mode=app&lang='+(fr()?'fr':'en');if(reload||!catalogueUrl){catalogueUrl=next;frame.src=next;}}
+ 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(error.message==='PRIVILEGED_APP_UNSUPPORTED'?
+ text('Cette application demande des API privilégiées qui ne sont pas disponibles dans Vulpes OS.',
+ 'This app requires privileged APIs that are not available in Vulpes OS.'):
+ 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(true);});
+ 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/overrides/search.gaiamobile.org/shared/resources/branding/browser_126.png b/overrides/search.gaiamobile.org/shared/resources/branding/browser_126.png
new file mode 100644
index 00000000..905faf81
Binary files /dev/null and b/overrides/search.gaiamobile.org/shared/resources/branding/browser_126.png differ
diff --git a/overrides/search.gaiamobile.org/shared/resources/branding/browser_142.png b/overrides/search.gaiamobile.org/shared/resources/branding/browser_142.png
new file mode 100644
index 00000000..7a3346bf
Binary files /dev/null and b/overrides/search.gaiamobile.org/shared/resources/branding/browser_142.png differ
diff --git a/overrides/search.gaiamobile.org/shared/resources/branding/browser_189.png b/overrides/search.gaiamobile.org/shared/resources/branding/browser_189.png
new file mode 100644
index 00000000..7bfb5074
Binary files /dev/null and b/overrides/search.gaiamobile.org/shared/resources/branding/browser_189.png differ
diff --git a/overrides/search.gaiamobile.org/shared/resources/branding/browser_256.png b/overrides/search.gaiamobile.org/shared/resources/branding/browser_256.png
new file mode 100644
index 00000000..f665bfe1
Binary files /dev/null and b/overrides/search.gaiamobile.org/shared/resources/branding/browser_256.png differ
diff --git a/overrides/search.gaiamobile.org/shared/resources/branding/browser_284.png b/overrides/search.gaiamobile.org/shared/resources/branding/browser_284.png
new file mode 100644
index 00000000..21913992
Binary files /dev/null and b/overrides/search.gaiamobile.org/shared/resources/branding/browser_284.png differ
diff --git a/overrides/search.gaiamobile.org/shared/resources/branding/browser_336.png b/overrides/search.gaiamobile.org/shared/resources/branding/browser_336.png
new file mode 100644
index 00000000..3e15ea49
Binary files /dev/null and b/overrides/search.gaiamobile.org/shared/resources/branding/browser_336.png differ
diff --git a/overrides/search.gaiamobile.org/shared/resources/branding/browser_84.png b/overrides/search.gaiamobile.org/shared/resources/branding/browser_84.png
new file mode 100644
index 00000000..5089aa37
Binary files /dev/null and b/overrides/search.gaiamobile.org/shared/resources/branding/browser_84.png differ
diff --git a/overrides/settings.gaiamobile.org/js/panels/app_permissions_list/panel.js b/overrides/settings.gaiamobile.org/js/panels/app_permissions_list/panel.js
new file mode 100644
index 00000000..73657caf
--- /dev/null
+++ b/overrides/settings.gaiamobile.org/js/panels/app_permissions_list/panel.js
@@ -0,0 +1,366 @@
+
+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 = {
+ /**
+ * initialization
+ *
+ * @memberOf PermissionsList
+ * @param {HTMLElement} listRoot
+ * @access public
+ */
+ 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);
+ },
+
+ /**
+ * Set /resources/permissions_table.json.
+ *
+ * @memberOf PermissionsList
+ * @param {Object} permissionTable
+ * @access public
+ */
+ setPermissionsTable: function pl_setPermissionsTable(permissionTable) {
+ this._permissionTable = permissionTable;
+ },
+
+ /**
+ * Refresh the app list when we enter into panel.
+ *
+ * @memberOf PermissionsList
+ * @access public
+ * @return {Promise}
+ */
+ 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();
+ });
+ }
+ },
+
+ /**
+ * Go to app_permissions_detail panel when user select an app.
+ *
+ * @memberOf PermissionsList
+ * @param {Event} evt
+ * @access public
+ */
+ _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
+ });
+ }
+ },
+
+ /**
+ * When new application is installed, we push the app to list, sort them and
+ * rerender the app list.
+ *
+ * @memberOf PermissionsList
+ * @param {Event} evt
+ * @access public
+ */
+ _onApplicationInstall: function pl__onApplicationInstall(evt) {
+ var app = evt.application;
+ this._apps.push(app);
+ this._sortApps();
+ this.renderList();
+ },
+
+ /**
+ * When application is uninstalled, we remove it from list and rerender the
+ * app list.
+ *
+ * @memberOf PermissionsList
+ * @param {Event} evt
+ * @access public
+ */
+ _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();
+ },
+
+ /**
+ * Sort the applist by the name of its manifest.
+ *
+ * @memberOf PermissionsList
+ * @access private
+ */
+ _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;
+ });
+ },
+
+ /**
+ * Genrate UI template of app item.
+ *
+ * @memberOf PermissionsList
+ * @access private
+ * @param {Object} itemData
+ * @return {HTMLDivElement}
+ */
+ _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;
+ },
+
+ /**
+ * Genrate UI template of app item.
+ *
+ * @memberOf PermissionsList
+ * @access public
+ */
+ 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);
+ },
+
+ /**
+ * Genrate explicitCertifiedPermissions table from plainPermissions table
+ * table and "composedPermissions + accessModes" table.
+ *
+ * @memberOf PermissionsList
+ * @param {Event} evt
+ * @access private
+ */
+ _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;
+ },
+
+ /**
+ * Identify the permission whether is explict or not.
+ *
+ * @memberOf PermissionsList
+ * @access private
+ * @return {Bool}
+ */
+ _isExplicitPerm: function pl_isExplicitPerm(perm) {
+ return mozPerms && mozPerms.isExplicit(perm, this._currentApp.manifestURL,
+ this._currentApp.origin, false);
+ },
+
+ /**
+ * Filter explicit apps from moz apps, sort them, and render to screen.
+ *
+ * @memberOf PermissionsList
+ * @access public
+ * @return {Promise}
+ */
+ loadApps: function pl_loadApps() {
+ var self = this;
+ return AppsCache.apps().then(function(apps) {
+ self._loadApps(apps);
+ });
+ },
+
+ /**
+ * Iterate internal apps and render them on UI.
+ *
+ * @memberOf PermissionsList
+ * @param {Object[]} apps
+ * @access private
+ */
+ _loadApps: function pl__loadApps(apps) {
+ var table = this._permissionTable;
+ var internalRoles = ['system', 'input', 'homescreen', 'search', 'addon', 'langpack'];
+ apps.forEach(function(app) {
+ var manifest = app.manifest ? app.manifest : app.updateManifest;
+ if (manifest.role && internalRoles.indexOf(manifest.role) !== -1) {
+ return;
+ }
+ 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();
+ },
+
+ /**
+ * Get icon URL.
+ *
+ * @memberOf PermissionsList
+ * @param {Object} app
+ * @param {Object} icons
+ * @access private
+ */
+ _getBestIconURL: function pl__getBestIconURL(app, icons) {
+ if (!icons || !Object.keys(icons).length) {
+ return '../style/images/default.png';
+ }
+
+ // The preferred size is 30 by the default. If we use HDPI device, we may
+ // use the image larger than 30 * 1.5 = 45 pixels.
+ var preferredIconSize = 30 * (window.devicePixelRatio || 1);
+ var preferredSize = Number.MAX_VALUE;
+ var max = 0;
+
+ for (var size in icons) {
+ size = parseInt(size, 10);
+ if (size > max) {
+ max = size;
+ }
+
+ if (size >= preferredIconSize && size < preferredSize) {
+ preferredSize = size;
+ }
+ }
+ // If there is an icon matching the preferred size, we return the result,
+ // if there isn't, we will return the maximum available size.
+ if (preferredSize === Number.MAX_VALUE) {
+ preferredSize = max;
+ }
+
+ var url = icons[preferredSize];
+
+ if (url) {
+ return !(/^(http|https|data):/.test(url)) ? app.origin + url : url;
+ } else {
+ return '../style/images/default.png';
+ }
+ }
+ };
+
+ return function ctor_permissions_list() {
+ return new PermissionsList();
+ };
+});
+define('panels/app_permissions_list/panel',['require','modules/settings_panel','panels/app_permissions_list/app_permissions_list','shared/lazy_loader'],function(require){'use strict';var SettingsPanel=require('modules/settings_panel');var PermissionList=require('panels/app_permissions_list/app_permissions_list');var LazyLoader=require('shared/lazy_loader');return function ctor_app_permissions_list_panel(){var permissionsTableHasBeenLoaded=false;var elements={};var permissionListModule=PermissionList();return SettingsPanel({onInit:function(panel){elements={list:panel.querySelector('.app-list')};permissionListModule.init(elements.list);},onBeforeShow:function(){if(permissionsTableHasBeenLoaded){permissionListModule.refresh();}else{LazyLoader.getJSON('/resources/permissions_table.json').then(function(data){permissionsTableHasBeenLoaded=true;permissionListModule.setPermissionsTable(data);permissionListModule.refresh();});}
+permissionListModule.enabled=true;},onBeforeHide:function(){permissionListModule.enabled=false;}});};});
\ No newline at end of file
diff --git a/overrides/settings.gaiamobile.org/js/panels/battery/panel.js b/overrides/settings.gaiamobile.org/js/panels/battery/panel.js
new file mode 100644
index 00000000..b346536d
--- /dev/null
+++ b/overrides/settings.gaiamobile.org/js/panels/battery/panel.js
@@ -0,0 +1,75 @@
+/**
+ * The battery panel displays battery information provided by Battery.
+ */
+define(function(require) {
+ 'use strict';
+
+ var SettingsPanel = require('modules/settings_panel');
+ var Battery = require('modules/battery');
+
+ return function ctor_battery_panel() {
+ var _batteryLevelText = null;
+ var _batteryTimeText = null;
+ var _getBatteryTimeL10n = function(type) {
+ type = type === 'charging' ? 'charging': 'discharging';
+
+ var time = Battery[type + 'Time'];
+ var l10nAttrs = {
+ id: 'battery-' + type + '-calculating',
+ args: {}
+ };
+
+ if (time !== Infinity && time > 0) {
+ var timeLeft = new Date(0, 0, 0, 0, 0, time);
+ l10nAttrs.args.hours = timeLeft.getHours();
+ l10nAttrs.args.minutes = timeLeft.getMinutes();
+
+ if (l10nAttrs.args.hours > 0) {
+ l10nAttrs.id = 'battery-' + type + '-hours';
+ } else {
+ l10nAttrs.id = 'battery-' + type + '-minutes';
+ }
+ } else if (time !== Infinity && time === 0) {
+ l10nAttrs.id = 'battery-' + type + '-complete';
+ } else {
+ l10nAttrs.id = 'battery-' + type + '-unavailable';
+ }
+
+ return l10nAttrs;
+ };
+ var _refreshText = function() {
+ document.l10n.setAttributes(_batteryLevelText,
+ 'battery-current-level', { level: Battery.level });
+
+ if (Battery.state === 'unplugged') {
+ var l10nDischarging = _getBatteryTimeL10n('discharging');
+ document.l10n.setAttributes(_batteryTimeText,
+ l10nDischarging.id, l10nDischarging.args);
+ }
+ else {
+ var l10nCharging = _getBatteryTimeL10n('charging');
+ document.l10n.setAttributes(_batteryTimeText,
+ l10nCharging.id, l10nCharging.args);
+ }
+ };
+
+ return SettingsPanel({
+ onInit: function bp_onInit(rootElement) {
+ _batteryLevelText = rootElement.querySelector(
+ '#battery-level');
+ _batteryTimeText = rootElement.querySelector(
+ '#battery-time');
+ },
+ onBeforeShow: function bp_onBeforeShow(rootElement) {
+ Battery.observe('level', _refreshText);
+ Battery.observe('state', _refreshText);
+ Battery.observe('chargingTime', _refreshText);
+ Battery.observe('dischargingTime', _refreshText);
+ _refreshText();
+ },
+ onBeforeHide: function bp_onBeforeHide() {
+ Battery.unobserve(_refreshText);
+ }
+ });
+ };
+});
diff --git a/overrides/shared/style/value_selector.css b/overrides/shared/style/value_selector.css
new file mode 100644
index 00000000..cc906501
--- /dev/null
+++ b/overrides/shared/style/value_selector.css
@@ -0,0 +1,240 @@
+/* ----------------------------------
+ * Value selector (Single & Multiple)
+ * ---------------------------------- */
+
+/* Main dialog setup */
+[role="dialog"][data-type="value-selector"] {
+ background: #2d2d2d;
+ overflow: hidden;
+ position: absolute;
+ z-index: 100;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ padding: 0 0 7rem;
+ color: #fff;
+ font-family: sans-serif;
+}
+
+[role="dialog"][data-type="value-selector"] > section {
+ padding: 0 1.5rem;
+ box-sizing: padding-box;
+ width: 100%;
+ height: 100%;
+}
+
+[role="dialog"][data-type="value-selector"] h1 {
+ font-weight: 400;
+ font-size: 1.9rem;
+ line-height: 4.8rem;
+ color: #fff;
+ border-bottom: 0.1rem solid #616262;
+ background-color: rgba(0, 0, 0, .2);
+ margin: 0 -1.5rem;
+ padding: 0 3rem 1rem;
+ height: 4.8rem;
+ box-sizing: border-box;
+}
+
+/* Specific component code */
+[role="dialog"][data-type="value-selector"] [role="listbox"] {
+ position: relative;
+ padding: 0;
+ margin: 0 -1.5rem;
+ max-height: calc(100% - 5rem);
+ overflow: auto;
+}
+
+[role="dialog"][data-type="value-selector"] [role="listbox"] li {
+ margin: 0;
+ padding: 0 1.5rem;
+ height: auto;
+ list-style: none;
+ position: relative;
+ font-weight: 300;
+ font-size: 2.2rem;
+ line-height: normal;
+ color: #fff;
+ transition: background-color 0.2s ease;
+}
+
+html[dir="ltr"] [role="dialog"][data-type="value-selector"] [role="listbox"],
+html[dir="ltr"] [role="dialog"][data-type="value-selector"] [role="listbox"] li {
+ text-align: left;
+}
+html[dir="rtl"] [role="dialog"][data-type="value-selector"] [role="listbox"],
+html[dir="rtl"] [role="dialog"][data-type="value-selector"] [role="listbox"] li {
+ text-align: right;
+}
+
+[role="dialog"][data-type="value-selector"] [role="listbox"] li:first-child label {
+ border-top-color: transparent;
+}
+
+[role="dialog"][data-type="value-selector"] [role="listbox"] li label {
+ outline: none;
+ display: block;
+ color: #fff;
+ border-top: 0.1rem solid #666;
+}
+
+[role="dialog"][data-type="value-selector"] [role="listbox"] li label span {
+ display: block;
+ box-sizing: border-box;
+ min-height: 5.2rem;
+ padding: 1.2rem 1.5rem;
+ line-height: 2.8rem;
+ word-wrap: break-word;
+}
+
+/* Pressed status */
+[role="dialog"][data-type="value-selector"] [role="listbox"] li:active {
+ background-color: #00ABCC;
+}
+
+[role="dialog"][data-type="value-selector"] [role="listbox"] li:active label,
+[role="dialog"][data-type="value-selector"] [role="listbox"] li:active + li label {
+ border-top-color: transparent;
+}
+
+[role="dialog"][data-type="value-selector"] [role="listbox"] li:active label span {
+ color: #fff !important;
+ background-image: none;
+}
+
+/* Subheaders */
+[role="dialog"][data-type="value-selector"] [role="listbox"] li[role="subheader"] {
+ border: none;
+ background-color: #242424;
+ pointer-events: none;
+ -moz-user-select: none;
+}
+
+[role="dialog"][data-type="value-selector"] [role="listbox"] li[role="subheader"] label {
+ color: #f4f4f4;
+ font-size: 1.4rem;
+ font-weight: 500;
+}
+
+[role="dialog"][data-type="value-selector"] [role="listbox"] li[role="subheader"] label,
+[role="dialog"][data-type="value-selector"] [role="listbox"] li[role="subheader"] + li label {
+ border: none;
+}
+
+[role="dialog"][data-type="value-selector"] [role="listbox"] li[role="subheader"] label span {
+ line-height: 2rem;
+}
+
+/* Checked status */
+[role="dialog"][data-type="value-selector"] [role="listbox"] li[aria-selected="true"] label {
+ background-image: url(value_selector/images/icons/checked.png);
+ background-color: transparent;
+ background-size: 2.4rem;
+ background-repeat: no-repeat;
+}
+html[dir="ltr"] [role="dialog"][data-type="value-selector"] [role="listbox"] li[aria-selected="true"] label {
+ background-position: right 1.2rem top 50%;
+}
+html[dir="rtl"] [role="dialog"][data-type="value-selector"] [role="listbox"] li[aria-selected="true"] label {
+ background-position: left 1.2rem top 50%;
+}
+
+[role="dialog"][data-type="value-selector"] [role="listbox"] li[aria-selected="true"] span {
+ color: #00abcd;
+}
+html[dir="ltr"] [role="dialog"][data-type="value-selector"] [role="listbox"] li[aria-selected="true"] span {
+ padding-right: 2.6rem;
+ margin-right: 1.2rem;
+}
+html[dir="rtl"] [role="dialog"][data-type="value-selector"] [role="listbox"] li[aria-selected="true"] span {
+ padding-left: 2.6rem;
+ margin-left: 1.2rem;
+}
+
+/* Menu & buttons setup */
+.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons {
+ white-space: nowrap;
+ margin: 0;
+ padding: 1.5rem;
+ background: #4d4d4d;
+ display: block;
+ overflow: hidden;
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button::-moz-focus-inner {
+ border: none;
+ outline: none;
+}
+
+.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button {
+ width: calc((100% - 1rem) / 2);
+ font-family: sans-serif;
+ font-style: italic;
+ height: 4rem;
+ margin: 0 0 1rem;
+ padding: 0 1.2rem;
+ box-sizing: border-box;
+ display: inline-block;
+ vertical-align: middle;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ background: #d8d8d8;
+ border: none;
+ border-radius: 2rem;
+ font-weight: normal;
+ font-size: 1.6rem;
+ line-height: 4rem;
+ color: #333;
+ text-align: center;
+ text-shadow: none;
+ text-decoration: none;
+ outline: none;
+}
+
+/* Affirmative */
+.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button.affirmative,
+.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button.recommend {
+ background-color: #00caf2;
+ color: #fff;
+}
+
+/* Pressed */
+.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button:active {
+ background: #00aacc;
+ color: #fff;
+}
+
+/* Disabled */
+[role="dialog"][data-type="value-selector"] > menu.value-selector-buttons > button[disabled] {
+ background-color: #565656;
+ color: rgba(255,255,255,0.4);
+ pointer-events: none;
+}
+
+[role="dialog"][data-type="value-selector"] > menu.value-selector-buttons > button[disabled].recommend {
+ background-color: #006579;
+}
+
+button[disabled]::-moz-selection {
+ -moz-user-select: none;
+}
+
+.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button:last-child {
+ margin-inline-start: 1rem;
+}
+
+.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button,
+.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button:first-child {
+ margin: 0;
+}
+
+.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button.full {
+ width: 100%;
+}
+
diff --git a/overrides/system.gaiamobile.org/js/app_chrome.js b/overrides/system.gaiamobile.org/js/app_chrome.js
index 3890901d..b69d892e 100644
--- a/overrides/system.gaiamobile.org/js/app_chrome.js
+++ b/overrides/system.gaiamobile.org/js/app_chrome.js
@@ -475,20 +475,25 @@
}
}
- // Set the .icon property before saving for
- // backwards compatibility with verticalhome
+ // Set the .icon property before saving for backwards compatibility with
+ // verticalhome. A missing favicon must not prevent pinning the site.
+ var saveSite = function(icon) {
+ siteObject.icon = icon || '';
+ BookmarksDatabase.put(siteObject, siteObject.id)
+ .then(() => {
+ this.app.debug('pinSite: ' + siteObject.id);
+ this.systemBanner.show('site-pinned-to-home-screen');
+ })
+ .catch((error) => {
+ console.error('pinSite, Failed to pin site: ' + error);
+ });
+ }.bind(this);
+
IconsHelper.getIcon(siteObject.url, null,
- {icons: this.app.favicons}, siteObject).then(icon => {
- siteObject.icon = icon;
- BookmarksDatabase.put(siteObject, siteObject.id)
- .then(() => {
- this.app.debug('pinSite: ' + siteObject.id);
- this.systemBanner.show('site-pinned-to-home-screen');
- })
- .catch((error) => {
- console.error('pinSite, Failed to pin site: ' + error);
- });
- });
+ {icons: this.app.favicons}, siteObject).then(saveSite, function(error) {
+ this.app.debug('pinSite: using no favicon: ' + error);
+ saveSite('');
+ }.bind(this));
};
/**
@@ -986,12 +991,14 @@
return;
}
- // Enable/disable the bookmark option
- BookmarksDatabase.get(this._currentURL).then(function resolve(result) {
+ // start_url is the bookmark key for installed web apps, while _currentURL
+ // may be a later page in the app. Check the canonical key first.
+ var siteUrl = this.getSiteUrl();
+ BookmarksDatabase.get(siteUrl).then(function resolve(result) {
this.addToHomeButton.hidden = !!result;
}.bind(this),
function reject() {
- this.addToHomeButton.hidden = true;
+ this.addToHomeButton.hidden = false;
}.bind(this));
};
diff --git a/overrides/system.gaiamobile.org/style/app_install_manager/app_install_manager.css b/overrides/system.gaiamobile.org/style/app_install_manager/app_install_manager.css
new file mode 100644
index 00000000..320af8d0
--- /dev/null
+++ b/overrides/system.gaiamobile.org/style/app_install_manager/app_install_manager.css
@@ -0,0 +1,173 @@
+/* dialog */
+/* FIXME we have to see why we need such specific style here and why
+ * the general style in confirm.css isn't sufficient */
+#setup-installed-app-dialog,
+#ime-layout-dialog,
+#app-install-dialog,
+#app-install-cancel-dialog,
+#app-download-cancel-dialog {
+ display: none;
+ top: 3rem;
+ pointer-events: none;
+}
+
+/* Shift the bottom of the prompt to account for the height of the
+ software home button */
+#screen.software-button-enabled #setup-installed-app-dialog,
+#screen.software-button-enabled #ime-layout-dialog,
+#screen.software-button-enabled #app-install-dialog,
+#screen.software-button-enabled #app-install-cancel-dialog,
+#screen.software-button-enabled #app-download-cancel-dialog {
+ bottom: var(--software-home-button-height);
+}
+
+@media (orientation: landscape) {
+ #screen.software-button-enabled #setup-installed-app-dialog,
+ #screen.software-button-enabled #ime-layout-dialog,
+ #screen.software-button-enabled #app-install-dialog,
+ #screen.software-button-enabled #app-install-cancel-dialog,
+ #screen.software-button-enabled #app-download-cancel-dialog {
+ bottom: 0;
+ /* BiDi note: the software home button should not be mirrored in RTL. */
+ right: var(--software-home-button-height);
+ }
+}
+
+#setup-installed-app-dialog.visible,
+#ime-layout-dialog.visible,
+#app-install-dialog.visible,
+#app-install-cancel-dialog.visible,
+#app-download-cancel-dialog.visible {
+ display: inline-block;
+ pointer-events: auto;
+}
+
+#app-uninstall-dialog {
+ position: absolute;
+}
+
+#setup-installed-app-dialog section,
+#ime-layout-dialog section,
+#app-install-dialog section,
+#app-install-cancel-dialog section,
+#app-download-cancel-dialog section {
+ height: auto;
+}
+
+#setup-installed-app-dialog h1,
+#ime-layout-dialog h1,
+#app-install-dialog h1,
+#app-install-cancel-dialog h1,
+#app-download-cancel-dialog h1 {
+ border-bottom: none;
+ background: none;
+}
+
+#app-install-dialog dl > dt {
+ width: 8rem;
+ margin-inline-end: 1rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+#app-install-dialog p {
+ font-size: 1.4rem;
+ font-weight: 400;
+ line-height: normal;
+ border-top: none;
+}
+#app-install-dialog #system-addon-warning p + p {
+ margin-top: 0;
+}
+#app-install-dialog span {
+ font-size: 1.4rem;
+ font-weight: 400;
+ line-height: normal;
+ color: inherit;
+}
+#app-install-dialog #system-addon-icon {
+ height: 3rem;
+ width: 3rem;
+}
+html[dir="ltr"] #app-install-dialog #system-addon-icon {
+ float: left;
+ margin: 1.5rem 0 0 1.5rem;
+}
+html[dir="rtl"] #app-install-dialog #system-addon-icon {
+ float: right;
+ margin: 1.5rem 1.5rem 0 0;
+}
+
+#app-install-author-url {
+ direction: ltr;
+}
+
+#app-install-cancel-dialog small:first-child {
+ margin-bottom: 1rem;
+}
+
+/* ... end of dialog */
+
+/* notification */
+#install-manager-notification-container > .fake-notification {
+ position: relative;
+ border-bottom: none;
+}
+
+#install-manager-notification-container > .fake-notification > * {
+ pointer-events: none;
+}
+
+#install-manager-notification-container [data-icon] {
+ color: #9ab0c0;
+ background-color: #edf1f4;
+}
+
+#install-manager-notification-container .title-container {
+ margin: 1.4rem 0 0;
+ line-height: 3rem;
+}
+html[dir="ltr"] #install-manager-notification-container .title-container {
+ margin-left: 3.7rem;
+}
+html[dir="rtl"] #install-manager-notification-container .title-container {
+ margin-right: 3.7rem;
+}
+
+#install-manager-notification-container progress {
+ position: absolute;
+ width: 100%;
+ height: 0.4rem;
+ left: 0;
+ top: 5.6rem;
+ border: none;
+}
+
+#install-manager-notification-container progress::-moz-progress-bar {
+ background: url(../shared/progress.gif) repeat-x;
+}
+
+/* ... end of notification */
+
+/* ... start of keyboard layout */
+ul#ime-list {
+ padding-inline-start: 1.5rem;
+}
+
+ul#ime-list > li {
+ /* Positioning */
+ position: relative;
+
+ /* Display & Box Model */
+ width: 100%;
+ height: 3rem;
+ padding-top: 2rem;
+
+ /* Other */
+ list-style: none;
+}
+
+ul#ime-list > li label {
+ text-transform: none;
+}
+
+/* ... end of keyboard layout */
diff --git a/overrides/system.gaiamobile.org/style/system/initlogo.css b/overrides/system.gaiamobile.org/style/system/initlogo.css
index 7943c17e..a350c2ac 100644
--- a/overrides/system.gaiamobile.org/style/system/initlogo.css
+++ b/overrides/system.gaiamobile.org/style/system/initlogo.css
@@ -57,6 +57,6 @@
#os-logo { background:#071d35; display:flex; flex-direction:column; align-items:center; justify-content:center; }
#os-logo::before { content:""; width:min(42vw,190px); aspect-ratio:1; flex:none; background:url('/shared/resources/branding/vulpes-symbol.png') center / contain no-repeat; animation:vulpes-boot 2s ease-in-out infinite alternate; }
#os-logo::after { content:"VULPES\A Firefox OS Community Project"; white-space:pre-line; padding:12px 22px; color:#f5f8fa; font:500 20px/1.8 FiraSans,sans-serif; text-align:center; }
-#poweroff-splash { background:#071d35 url('/shared/resources/branding/vulpes-symbol.png') center / 190px 190px no-repeat; }
+#poweroff-splash { background:#071d35; }
@keyframes vulpes-boot { from {opacity:.75} to {opacity:1} }
@media (prefers-reduced-motion: reduce) { #os-logo::before {animation:none} }
diff --git a/release.json b/release.json
index b3606ca1..d170c6e1 100644
--- a/release.json
+++ b/release.json
@@ -1,15 +1,15 @@
{
"gaia": "2.7.0",
"channel": "preview",
- "revision": 22,
- "androidVersionCode": 22,
+ "revision": 24,
+ "androidVersionCode": 24,
"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.22",
+ "version": "2.7-preview.24",
"adapters": {
"desktop": "0.1.0-dev",
"android": "0.1.0-dev"
diff --git a/services/activities.mjs b/services/activities.mjs
index b5700bf5..cd3e2c1b 100644
--- a/services/activities.mjs
+++ b/services/activities.mjs
@@ -1,6 +1,7 @@
export function isGaiaActivity(source) {
return source?.name === 'pick' || source?.name === 'share' ||
(source?.name === 'moz_configure_window' && source.data?.target === 'device') ||
+ (source?.name === 'configure' && source.data?.target === 'device') ||
(source?.name === 'configure' && source.data?.target === 'ringtone') ||
(source?.name === 'dial' && source.data?.type === 'webtelephony/number') ||
(source?.name === 'new' && source.data?.type === 'websms/sms') ||
@@ -33,6 +34,7 @@ export class Activities {
else if (types.some(t=>typeof t==='string' && t.startsWith('audio/'))) id='music';
else if (types.some(t=>typeof t==='string' && t.startsWith('video/'))) id='video';
} else if (name === 'configure' && source.data?.target === 'ringtone') id='ringtones';
+ else if (name === 'configure' && source.data?.target === 'device') id='settings';
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';
diff --git a/services/packages.mjs b/services/packages.mjs
index ce8fa674..5a5fa125 100644
--- a/services/packages.mjs
+++ b/services/packages.mjs
@@ -51,6 +51,8 @@ export async function unpackPackage(bytes, source) {
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');
+ if (original.type === 'privileged' || original.type === 'certified')
+ throw Error('PRIVILEGED_APP_UNSUPPORTED');
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={};
diff --git a/tests/keypad-browser.test.mjs b/tests/keypad-browser.test.mjs
index c9a7929a..3923d71b 100644
--- a/tests/keypad-browser.test.mjs
+++ b/tests/keypad-browser.test.mjs
@@ -59,6 +59,20 @@ test('toolbar focus does not select a bar hidden behind the lockscreen or a dial
assert.equal(selected,1);
});
+test('Up reopens the browser toolbar when the collapsed rocketbar search input kept focus',()=>{
+ let selected=0;
+ const bar={checkVisibility:()=>true,getBoundingClientRect:()=>({x:0,y:20,width:200,height:40}),
+ contains:()=>true};
+ const child=actor('KeypadChild');
+ child.document={location:{hostname:'system.localhost'},activeElement:{id:'rocketbar-input',localName:'input',type:'search'},
+ querySelector:()=>({classList:{contains:()=>false}}),querySelectorAll:()=>[bar],elementFromPoint:()=>bar,
+ documentElement:{dataset:{}}};
+ child.contentWindow={innerWidth:240,innerHeight:320};
+ child.select=()=>selected++;
+ child.receiveMessage({name:'Vulpes:Navigate',data:{key:'ArrowUp'}});
+ assert.equal(selected,1);
+});
+
test('Down leaves the toolbar and routes to its page instead of retaining toolbar focus',()=>{
let focused=false;
const messages=[];
diff --git a/tests/native-camera.test.mjs b/tests/native-camera.test.mjs
index de2b40f3..aa02890f 100644
--- a/tests/native-camera.test.mjs
+++ b/tests/native-camera.test.mjs
@@ -5,10 +5,10 @@ import vm from 'node:vm';
function camera() {
const workers = [];
- const context = {setTimeout, clearTimeout, Subprocess: {async call() {
+ const context = {setTimeout, clearTimeout, Subprocess: {async call(options) {
let finish;
const done = new Promise(resolve => { finish = resolve; });
- const worker = {closed:false, writes:[],
+ const worker = {closed:false, writes:[], options,
stdin:{async close() {worker.closed=true;finish();}, async write(text) {worker.writes.push(text);}},
wait:()=>done, kill:()=>finish()};
workers.push(worker); return worker;
@@ -16,15 +16,15 @@ function camera() {
const source = readFileSync(new URL('../host/NativeCamera.sys.mjs', import.meta.url),'utf8')
.replace(/^import .*;\n/gm,'').replace('export class NativeCamera','globalThis.NativeCamera = class NativeCamera');
vm.runInNewContext(source,context);
- const service = new context.NativeCamera();
- service.packet = async () => ({ready:true,format:'nv21',width:640,height:480});
+ const service = new context.NativeCamera('beatles');
+ service.packet = async () => ({ready:true,format:'nv21',...service.previewSize});
return {service,workers};
}
test('a delayed stop or frame from the old camera cannot close its replacement',async()=>{
const {service,workers}=camera(), actor={};
const old=await service.request(actor,{action:'start',camera:'back'});
- const next=await service.request(actor,{action:'start',camera:'front'});
+ const next=await service.request(actor,{action:'start',camera:'back'});
assert.notEqual(old.session,next.session);assert.equal(workers[0].closed,true);
await service.request(actor,{action:'stop',session:old.session});
assert.equal(workers[1].closed,false);
@@ -36,6 +36,34 @@ test('a delayed stop or frame from the old camera cannot close its replacement',
assert.equal(workers[1].closed,true);
});
+test('Beatles uses the stock KaiOS 320x240 preview while other devices keep 640x480',async()=>{
+ const {service,workers}=camera(), actor={};
+ const metadata=await service.request(actor,{action:'start',camera:'back'});
+ assert.equal(service.previewSize.width,320);
+ assert.equal(service.previewSize.height,240);
+ assert.equal(metadata.width,320);
+ assert.equal(metadata.height,240);
+ assert.equal(metadata.frameRate,5);
+ assert.deepEqual([...metadata.cameras],['back']);
+ assert.equal(workers[0].options.command,'/usr/bin/python3.11');
+ assert.equal(workers[0].options.arguments[1],'/opt/vulpes/tundra/runtime/beatles-camera-client.py');
+ await service.request(actor,{action:'stop',session:metadata.session});
+
+ const sargo=new service.constructor('sargo');
+ assert.equal(sargo.previewSize.width,640);
+ assert.equal(sargo.previewSize.height,480);
+ sargo.packet=async()=>({ready:true,format:'nv21',...sargo.previewSize});
+ const sargoMetadata=await sargo.request(actor,{action:'start',camera:'back'});
+ assert.equal(sargoMetadata.frameRate,30);
+ await sargo.request(actor,{action:'stop',session:sargoMetadata.session});
+});
+
+test('Beatles reports the missing front sensor instead of opening its rear sensor twice',async()=>{
+ const {service,workers}=camera(), actor={};
+ await assert.rejects(service.request(actor,{action:'start',camera:'front'}),/CAMERA_UNAVAILABLE/);
+ assert.equal(workers.length,0);
+});
+
test('a different actor cannot stop or use the current camera',async()=>{
const {service,workers}=camera(), owner={}, other={};
const meta=await service.request(owner,{action:'start'});
diff --git a/tests/packages.test.mjs b/tests/packages.test.mjs
index 1c717192..98b46e26 100644
--- a/tests/packages.test.mjs
+++ b/tests/packages.test.mjs
@@ -13,9 +13,11 @@ 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 bytes=zip({}, {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 type of ['privileged','certified'])
+ await assert.rejects(unpackPackage(zip({}, {type}), source), /PRIVILEGED_APP_UNSUPPORTED/);
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));
@@ -24,7 +26,7 @@ const service=new Packages({save:async value=>saved=value,changed:async()=>chang
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');
+console.log('Package URL policy, ZIP boundaries/CRC, permission stripping, privileged-app rejection and one-use confirmation: OK');
// Cancellation during download must not produce a confirmation token.
let finishDownload;
diff --git a/tools/project.py b/tools/project.py
index 670f1cce..66809465 100644
--- a/tools/project.py
+++ b/tools/project.py
@@ -3,13 +3,16 @@
import json
from pathlib import Path
import subprocess
+import time
ROOT = Path(__file__).resolve().parents[1]
# Editable Gaia files shared by desktop serving and Android packaging.
SOURCE_OVERRIDES = {
"system": ["js/launcher.js", "js/gaia_scheduler.js", "js/keyboard_manager.js",
- "js/core.js", "js/rocketbar.js"],
+ "js/core.js", "js/rocketbar.js", "js/wrapper_factory.js",
+ "js/cover_screen.js", "js/lockscreen_window.js"],
+ "camera": ["js/views/setting-options.js", "js/views/setting.js"],
"communications": ["dialer/js/telephony_helper.js"],
"settings": ["js/modules/storage_helper.js", "js/startup.js"],
"keyboard": ["js/settings/layout_dictionary_downloader.js"] + [
@@ -42,7 +45,7 @@ def validate_assets():
def build_info():
"""Read this checkout's revision; archives without .git report it as unknown."""
- unknown = {"revision": "unknown", "timestamp": 0, "dirty": None}
+ unknown = {"revision": "unknown", "timestamp": int(time.time()), "dirty": None}
if not (ROOT / ".git").exists():
return unknown
try:
diff --git a/tools/refresh-camera-modules.py b/tools/refresh-camera-modules.py
index 19e054dc..01d0df3b 100644
--- a/tools/refresh-camera-modules.py
+++ b/tools/refresh-camera-modules.py
@@ -7,7 +7,18 @@ TARGET=ROOT/'overrides/camera.gaiamobile.org/js/main.js'
dcf=ROOT/'overrides/camera.gaiamobile.org/js/lib/dcf.js'
dcf.parent.mkdir(parents=True,exist_ok=True)
dcf.write_text((ROOT/'gaia/apps/camera/js/lib/dcf.js').read_text())
-for name in ['lib/storage','controllers/storage','controllers/activity']:
+for name in [
+ 'app',
+ 'lib/camera/camera',
+ 'lib/storage',
+ 'controllers/activity',
+ 'controllers/battery',
+ 'controllers/overlay',
+ 'controllers/settings',
+ 'controllers/storage',
+ 'controllers/viewfinder',
+ 'views/viewfinder',
+]:
source=(ROOT/'gaia/apps/camera/js'/f'{name}.js').read_text()
deps=list(dict.fromkeys(re.findall(r"require\(['\"]([^'\"]+)['\"]\)",source)))
source=source.replace('define(function(require, exports, module)', 'define('+json.dumps(name)+','+json.dumps(['require','exports','module',*deps])+',function(require, exports, module)',1)
diff --git a/tools/refresh-settings-panels.py b/tools/refresh-settings-panels.py
index 4a5465f7..063e0115 100644
--- a/tools/refresh-settings-panels.py
+++ b/tools/refresh-settings-panels.py
@@ -18,6 +18,8 @@ modules = [
('modules/date_time', 'js/modules/date_time.js'),
('panels/downloads/download_api_manager', 'js/panels/downloads/panel.js'),
('panels/help/support', 'js/panels/help/panel.js'),
+ ('panels/app_permissions_list/app_permissions_list',
+ 'js/panels/app_permissions_list/panel.js'),
]
names=list(dict.fromkeys(name for name,relative in modules))
base=ROOT/'assets/webapps/settings.gaiamobile.org'
diff --git a/tools/test-apps.py b/tools/test-apps.py
index f2fe5eab..9a5fb85b 100644
--- a/tools/test-apps.py
+++ b/tools/test-apps.py
@@ -380,7 +380,7 @@ done(canvas.toDataURL());}).catch(e=>done({error:String(e)}));""",
"browser-localized-name",
ev(
"system",
- "const a=(await navigator.mozApps.mgmt.getAll()).find(a=>a.origin.includes('search.localhost'));return a.manifest.locales.fr.name==='Navigateur' && Object.values(a.manifest.icons).every(i=>i.includes('vulpes-browser.png'));",
+ "const a=(await navigator.mozApps.mgmt.getAll()).find(a=>a.origin.includes('search.localhost'));return a.manifest.locales.fr.name==='Navigateur' && Object.values(a.manifest.icons).every(i=>i.includes('/shared/resources/branding/browser_'));",
),
)
shot("home-transparent")
diff --git a/tundra/boot/native-root.sh b/tundra/boot/native-root.sh
index 1e9c2c31..df54d67b 100755
--- a/tundra/boot/native-root.sh
+++ b/tundra/boot/native-root.sh
@@ -9,7 +9,8 @@ if [ "$(date +%s)" -lt "${BUILD_EPOCH:-0}" ]; then date -s "@$BUILD_EPOCH"; fi
trap 'echo TUNDRA_NATIVE_SETUP_FAILED; while :; do /bin/busybox sleep 5; done' EXIT
LOWER=/installed/home/droidian/vulpes-tundra-tests/runtime-reference-20260925/root
if [ -n "${SYSTEM_IMAGE:-}" ]; then
- test "$(sha256sum "$SYSTEM_IMAGE" | cut -d' ' -f1)" = "$SYSTEM_IMAGE_SHA"
+ test -f "$SYSTEM_IMAGE" && test ! -L "$SYSTEM_IMAGE"
+ test "$(stat -c %s "$SYSTEM_IMAGE")" = "$SYSTEM_IMAGE_BYTES"
mkdir -p /run/tundra-system
mount -t ext4 -o loop,ro,noload "$SYSTEM_IMAGE" /run/tundra-system
LOWER=/run/tundra-system
diff --git a/tundra/boot/rescue-init.sh b/tundra/boot/rescue-init.sh
index faa7a517..b3d0a60e 100755
--- a/tundra/boot/rescue-init.sh
+++ b/tundra/boot/rescue-init.sh
@@ -45,7 +45,19 @@ done
/sbin/dropbear -E -s -j -k -p 10.15.19.82:2222 -r /etc/tundra-hostkey -P /run/dropbear.pid
echo TUNDRA_USB_SSH_READY
# No LVM activation, fsck, journal replay, resizing, or writes to a block device.
-if [ -b /dev/mmcblk0p72 ] && [ "$(dd if=/dev/mmcblk0p72 bs=1048576 count=1 2>/dev/null | sha256sum | cut -d' ' -f1)" = "$EXPECTED_STORAGE_HASH" ]; then
+if [ "${RAW_USERDATA_EXT4:-0}" = 1 ]; then
+ # Ext4 updates mutable mount counters and timestamps in the first MiB; use
+ # the filesystem UUID at the fixed superblock offset as the stable identity.
+ actual_uuid=$(dd if=/dev/mmcblk0p72 bs=1 skip=1128 count=16 2>/dev/null | od -An -tx1 | tr -d ' \n')
+ if [ -b /dev/mmcblk0p72 ] && [ "$actual_uuid" = "$EXPECTED_STORAGE_UUID_HEX" ]; then
+ mkdir -p /installed
+ if [ "${TUNDRA_USERDATA_RW:-0}" = 1 ]; then
+ mount -t ext4 -o rw /dev/mmcblk0p72 /installed
+ else
+ mount -t ext4 -o ro,noload /dev/mmcblk0p72 /installed
+ fi
+ fi
+elif [ -b /dev/mmcblk0p72 ] && [ "$(dd if=/dev/mmcblk0p72 bs=1048576 count=1 2>/dev/null | sha256sum | cut -d' ' -f1)" = "$EXPECTED_STORAGE_HASH" ]; then
mkdir -p /dev/mapper /installed
/sbin/dmsetup create tundra-installed-ro --readonly --table '0 104448000 linear /dev/mmcblk0p72 329728'
/sbin/dmsetup mknodes
diff --git a/tundra/boot/watchdog.c b/tundra/boot/watchdog.c
index d4a489f8..868a6742 100644
--- a/tundra/boot/watchdog.c
+++ b/tundra/boot/watchdog.c
@@ -5,8 +5,11 @@
#define _start unused_stage0_start
#include "init.c"
#undef _start
+#ifndef TUNDRA_WATCHDOG_SECONDS
+#define TUNDRA_WATCHDOG_SECONDS 600
+#endif
__attribute__((noreturn)) void _start(void) {
- long remaining[2] = {600, 0};
+ long remaining[2] = {TUNDRA_WATCHDOG_SECONDS, 0};
while (call(NR_nanosleep, (long)remaining, (long)remaining, 0, 0, 0) == -4) {}
say("TUNDRA_TRIAL_DEADLINE_REBOOT\n");
call(NR_reboot, 0xfee1dead, 672274793, 0x01234567, 0, 0);
diff --git a/tundra/devices/beatles/device.json b/tundra/devices/beatles/device.json
index 0a6341bf..9143d950 100644
--- a/tundra/devices/beatles/device.json
+++ b/tundra/devices/beatles/device.json
@@ -6,7 +6,7 @@
"abi": "armeabi-v7a",
"soc": "MSM8905",
"board": "msm8909",
- "taVariant": null,
+ "taVariant": "TA-1175",
"observedFirmware": {
"kaios": "2.5.2.2",
"gecko": "48.0a2",
@@ -128,7 +128,7 @@
"Predictive T9 lexicons are generated from Gaia keyboard wordlists; 12,000 frequent words per locale, loaded on demand. User-dictionary learning is not implemented"
],
"privateBootBuild": {
- "path": "tundra/out/beatles/boot-trial-20261018-r5",
+ "path": "tundra/out/beatles/boot-trial-20261022-r7",
"signed": false,
"bootTested": false,
"standalone": false,
diff --git a/tundra/devices/beatles/gaia-audit.sys.mjs b/tundra/devices/beatles/gaia-audit.sys.mjs
index 280aaa46..f25d924a 100644
--- a/tundra/devices/beatles/gaia-audit.sys.mjs
+++ b/tundra/devices/beatles/gaia-audit.sys.mjs
@@ -1,4 +1,5 @@
import {tundraRadio} from 'resource://vulpes/host/Tundra.sys.mjs';
+import {Runtime} from 'resource://vulpes/host/Runtime.sys.mjs';
import {setTimeout} from 'resource://gre/modules/Timer.sys.mjs';
const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
@@ -164,6 +165,62 @@ async function run(win) {
await key('Escape');
}
}
+ // Exercise the real Tundra worker with the exact 320x240 preview mode
+ // used by KaiOS on beatles. Keep the sample in memory and release Camera1
+ // immediately; this is a smoke check, not a retained photo or recording.
+ const cameraActor={};
+ let cameraSession;
+ try {
+ const metadata=await Runtime.platform.nativeCamera.request(cameraActor,{action:'start',camera:'back'});
+ cameraSession=metadata.session;
+ const frame=await Runtime.platform.nativeCamera.request(cameraActor,{action:'frame',session:cameraSession});
+ const data=new Uint8Array(frame.data);
+ const pixels=metadata.width*metadata.height;
+ let sum=0,min=255,max=0;
+ for(let i=0;i32};
+ const picture=await Runtime.platform.nativeCamera.request(cameraActor,{action:'picture',session:cameraSession,size:{width:1600,height:1200}});
+ const jpeg=new Uint8Array(picture.data);
+ report.cameraPicture={bytes:jpeg.length,jpeg:jpeg[0]===0xff&&jpeg[1]===0xd8,passed:jpeg.length>4096&&jpeg[0]===0xff&&jpeg[1]===0xd8};
+ } catch(error) {
+ report.cameraPreview={passed:false,error:error.message || String(error)};
+ } finally {
+ if(cameraSession!==undefined) {
+ try { await Runtime.platform.nativeCamera.request(cameraActor,{action:'stop',session:cameraSession}); }
+ catch(error) { report.cameraPreview={...report.cameraPreview,stopError:error.message || String(error)}; }
+ }
+ }
+ report.cameraApps=Runtime.apps.filter(app=>/camera|photo/i.test(app.id+' '+app.name+' '+app.origin)).map(app=>({id:app.id,name:app.name,origin:app.origin,manifestURL:app.manifestURL}));
+ try {
+ // Match the URL-based launcher used by the other Gaia app audits. The
+ // app registry's internal ID is not stable across Gaia manifests.
+ await evaluate('system',`return await VulpesCompat.call('apps.launch',{manifestURL:'http://camera.localhost:8765/manifest.webapp'});`);
+ for(let i=0;i<120&&!find(system,'camera');i++)await sleep(250);
+ if(find(system,'camera')) {
+ for(let i=0;i<120;i++) {
+ const state=await evaluate('camera',"return {ready:document.readyState==='complete',app:typeof app!=='undefined'&&!!app.camera,busy:typeof app!=='undefined'&&!!app.camera?.isBusy};");
+ if(state.ready&&state.app&&(!state.busy||i>=119))break;
+ await sleep(250);
+ }
+ await sleep(2500);
+ report.cameraApp=await evaluate('camera',"var v=document.querySelector('.js-video'),c=document.querySelector('canvas[data-vulpes-native-preview]'),a=typeof app==='undefined'?null:app,cam=a?.camera,stream=cam?.mozCamera,canvasLuma=null;try{if(c){const t=document.createElement('canvas');t.width=80;t.height=60;const x=t.getContext('2d',{willReadFrequently:true});x.drawImage(c,0,0,80,60);const p=x.getImageData(0,0,80,60).data;let sum=0,min=255,max=0;for(let i=0;i>8;sum+=y;min=Math.min(min,y);max=Math.max(max,y);}canvasLuma={mean:Number((sum/4800).toFixed(1)),min,max};}}catch(e){canvasLuma={error:e.message||String(e)};}return {loaded:document.readyState==='complete',readyState:document.readyState,url:location.href,title:document.title,viewfinder:!!v,previewCanvas:!!c,canvasSize:c?[c.width,c.height]:null,canvasLuma,videoHidden:v?getComputedStyle(v).display==='none':false,documentHidden:document.hidden,visibility:document.visibilityState,errorText:document.querySelector('.overlay[data-type=\"request-fail\"] #overlay-text')?.textContent,bodyText:document.body?.innerText.slice(0,240),appGlobal:!!a,cameraObject:!!cam,cameraBusy:cam?.isBusy,cameraReady:!!stream&&!cam.isBusy,mozCamera:!!stream,previewFrames:stream?.vulpesTiming?.frames||0,previewClosed:stream?.vulpesClosed,mode:cam?.mode||null,cameraList:cam?.cameraList};");
+ if(report.cameraApp.cameraReady) {
+ try {
+ await evaluate('camera',"await app.camera.mozCamera.vulpesCameraControl({action:'flash',mode:'torch'});return true;");
+ await sleep(1500);
+ report.cameraApp.torchCanvas=await evaluate('camera',"const c=document.querySelector('canvas[data-vulpes-native-preview]'),t=document.createElement('canvas');t.width=80;t.height=60;const x=t.getContext('2d',{willReadFrequently:true});x.drawImage(c,0,0,80,60);const p=x.getImageData(0,0,80,60).data;let sum=0,min=255,max=0;for(let i=0;i>8;sum+=y;min=Math.min(min,y);max=Math.max(max,y);}return {mean:Number((sum/4800).toFixed(1)),min,max};");
+ } catch(error) { report.cameraApp.torchCanvas={error:error.message||String(error)}; }
+ finally {
+ try { await evaluate('camera',"await app.camera.mozCamera.vulpesCameraControl({action:'flash',mode:'off'});return true;"); }
+ catch(error) { report.cameraApp.torchOffError=error.message||String(error); }
+ }
+ try { report.cameraApp.picture=await evaluate('camera',"const b=await app.camera.mozCamera.takePicture(),d=new Uint8Array(await b.arrayBuffer());let bitmap=null;try{bitmap=await createImageBitmap(b);const c=document.createElement('canvas');c.width=80;c.height=60;const x=c.getContext('2d',{willReadFrequently:true});x.drawImage(bitmap,0,0,c.width,c.height);const p=x.getImageData(0,0,c.width,c.height).data;let sum=0,min=255,max=0;for(let i=0;i>8;sum+=y;min=Math.min(min,y);max=Math.max(max,y);}return {bytes:b.size,type:b.type,jpeg:d[0]===255&&d[1]===216,width:bitmap.width,height:bitmap.height,lumaMean:Number((sum/(p.length/4)).toFixed(1)),lumaMin:min,lumaMax:max};}finally{bitmap?.close();}"); }
+ catch(error) { report.cameraApp.picture={error:error.message||String(error)}; }
+ }
+ } else report.cameraApp={loaded:false,launch:'sent',apps:report.cameraApps};
+ } catch(error) { report.cameraApp={loaded:false,error:error.message||String(error),apps:report.cameraApps}; }
}catch(error){report.error=String(error);report.stack=error.stack;}
await save();
}
diff --git a/tundra/devices/beatles/gaia-session.py b/tundra/devices/beatles/gaia-session.py
index 149f2f55..8ef8c031 100644
--- a/tundra/devices/beatles/gaia-session.py
+++ b/tundra/devices/beatles/gaia-session.py
@@ -50,8 +50,8 @@ for path in (ROOT/'assets').iterdir():
if path.is_file():os.chown(path,2000,2000)
-def unprivileged():
- Path('/proc/self/oom_score_adj').write_text('800')
+def unprivileged(oom_score=800):
+ Path('/proc/self/oom_score_adj').write_text(str(oom_score))
# Android's paranoid-network kernel requires inet even for private loopback.
os.setgroups([3003])
os.setgid(2000)
@@ -64,15 +64,15 @@ env.update(HOME='/work',TMPDIR='/tmp',DISPLAY=':0',MOZ_ENABLE_WAYLAND='0',
VULPES_FULLSCREEN='1',VULPES_KEYPAD='1',VULPES_NATIVE_TOUCH='1',
VULPES_DEVICE='beatles',VULPES_TUNDRA_STATUS='1',
MOZ_CRASHREPORTER_DISABLE='1',FONTCONFIG_PATH='/etc/fonts')
-duration=1780 if os.environ.get("VULPES_TRIAL_MANUAL")=="1" else 160
+duration=1780 if os.environ.get("VULPES_TRIAL_MANUAL")=="1" else 220
processes=[]
handles=[]
try:
- def start(command,log,drop=False):
+ def start(command,log,drop=False,oom_score=800):
path=Path('/work/'+log)
output=path.open('w');os.chmod(path,0o644);handles.append(output)
proc=subprocess.Popen(command,env=env,stdout=output,stderr=subprocess.STDOUT,
- preexec_fn=unprivileged if drop else None)
+ preexec_fn=(lambda: unprivileged(oom_score)) if drop else None)
processes.append(proc)
return proc
xorg=start(['/usr/lib/xorg/Xorg',':0','vt1','-config','/etc/X11/xorg.conf',
@@ -101,7 +101,7 @@ try:
with socket.create_connection(('127.0.0.1',8765),timeout=.2):break
except OSError:time.sleep(.1)
gecko=start(['/opt/firefox/firefox','-no-remote','-purgecaches','-profile',str(PROFILE),
- '-chrome','chrome://vulpes/content/shell.xhtml'],'gecko.log',True)
+ '-chrome','chrome://vulpes/content/shell.xhtml'],'gecko.log',True,oom_score=200)
begin=time.monotonic()
last_key_sequence=0
last_memory_sample=0
diff --git a/tundra/devices/sargo-p23-20261027/backup/manifest.json b/tundra/devices/sargo-p23-20261027/backup/manifest.json
new file mode 100644
index 00000000..80c9165a
--- /dev/null
+++ b/tundra/devices/sargo-p23-20261027/backup/manifest.json
@@ -0,0 +1,42 @@
+{
+ "recordedAt": "2026-09-24T18:33:08.938157+00:00",
+ "device": "sargo",
+ "rawPartitions": true,
+ "restorationTested": false,
+ "scope": "Boot, DTBO and vbmeta only; excludes userdata, vendor and system",
+ "files": {
+ "boot_a.img": {
+ "source": "/dev/mmcblk0p13",
+ "bytes": 67108864,
+ "sha256": "df961004d4d455f62635c080a3f2543cdf893a683626a0fb4da9104f69f70699"
+ },
+ "boot_b.img": {
+ "source": "/dev/mmcblk0p14",
+ "bytes": 67108864,
+ "sha256": "da4a4ba50191ed6fba8dda50c155c53e21c973ccf504f0a9aff0e2acffc48350"
+ },
+ "dtbo_a.img": {
+ "source": "/dev/mmcblk0p44",
+ "bytes": 8417280,
+ "sha256": "0e43a57d6423e8d24cd944fcad0939ed81160e847a6d4203e2f7c16a86d69c03"
+ },
+ "dtbo_b.img": {
+ "source": "/dev/mmcblk0p45",
+ "bytes": 8417280,
+ "sha256": "2453dbdb75d8e67e47ac531dc7a0b2cb36f00710b824e5d86019523f8d92cfe9"
+ },
+ "vbmeta_a.img": {
+ "source": "/dev/mmcblk0p63",
+ "bytes": 65536,
+ "sha256": "add7e48f84f3e98061f33aabb4cc678b6a631af66834f0fae579324b7acc681a"
+ },
+ "vbmeta_b.img": {
+ "source": "/dev/mmcblk0p64",
+ "bytes": 65536,
+ "sha256": "3e168134406526bc068e8f401231ce53b96f49ddf5a400a0a3ec25e118186cf2"
+ }
+ },
+ "serialSha256": "8c024ab1ad147c85b56ae800d741120291d1e5aaf97b81f6a935c0294845f6fe",
+ "activeSlot": "a",
+ "qualificationSha256": "e9ad84d2659114786b409eb27b6fba9c71bf6e5580252cd7297425500235face"
+}
diff --git a/tundra/devices/sargo-p23-20261027/device.json b/tundra/devices/sargo-p23-20261027/device.json
new file mode 100644
index 00000000..4237246d
--- /dev/null
+++ b/tundra/devices/sargo-p23-20261027/device.json
@@ -0,0 +1,14 @@
+{
+ "schema": 1,
+ "device": "sargo",
+ "activeSlot": "a",
+ "serialSha256": "8c024ab1ad147c85b56ae800d741120291d1e5aaf97b81f6a935c0294845f6fe",
+ "kernelSha256": "e214e5e363bcf4db6387668ba723299e75009bc128fb0088f706cfb311db0080",
+ "private": true,
+ "flashReady": false,
+ "sourceBackupQualificationSha256": "8b45d37a992c2f338a678b1962edeb17b62822d8c05f4e72cf7cde6b1c831044",
+ "files": {
+ "backup/manifest.json": "620f0f6ae0a582d00c61c8d8bfb452eb52a0c251e54287a85ec298f757199170",
+ "layout.json": "b4ec8d4ea3d8d5c23f51b6bd514984feb1a9aacfef563dccda2c59b0d667fd2b"
+ }
+}
diff --git a/tundra/devices/sargo-p23-20261027/layout.json b/tundra/devices/sargo-p23-20261027/layout.json
new file mode 100644
index 00000000..a5397ec7
--- /dev/null
+++ b/tundra/devices/sargo-p23-20261027/layout.json
@@ -0,0 +1,17 @@
+{
+ "schema": 2,
+ "device": "/dev/mmcblk0p72",
+ "table": "direct",
+ "size": "53648801280",
+ "blkid": "/dev/mmcblk0p72: UUID=\"fed927a8-c694-4c9b-a8ec-48ad5a71139b\" TYPE=\"ext4\" PARTLABEL=\"userdata\" PARTUUID=\"dc37ac3d-ddc2-4350-a86d-5adaf76fcd78\"",
+ "partitionLabel": "userdata",
+ "filesystem": "ext4",
+ "storage": "raw-userdata-ext4",
+ "filesystemUuid": "fed927a8-c694-4c9b-a8ec-48ad5a71139b",
+ "backingDevice": "259:40",
+ "firstMiBSha256": "dc8f304baa35a09f82fdc4caa862b3dd71edfe05da34279c7a2d78cadb88cf4b",
+ "activeSlot": "a",
+ "serialSha256": "8c024ab1ad147c85b56ae800d741120291d1e5aaf97b81f6a935c0294845f6fe",
+ "observed": "2026-10-27T22:xxZ via temporary rescue; read-only block inspection",
+ "observedAt": "2026-10-27T22:xx:xxZ (temporary rescue; block reads only)"
+}
diff --git a/tundra/probes/beatles-camera-broker.c b/tundra/probes/beatles-camera-broker.c
new file mode 100644
index 00000000..1724ab85
--- /dev/null
+++ b/tundra/probes/beatles-camera-broker.c
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: MPL-2.0
+// Fixed-command local broker; camera access is confined to Android UID 1006.
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define SOCKET_DIR "/data/local/vulpes-beatles/camera-ipc"
+#define SOCKET_PATH SOCKET_DIR "/camera.sock"
+#define WORKER "/data/local/tmp/vulpes-beatles-camera-worker"
+
+static volatile sig_atomic_t stopping;
+static void stop_now(int signal_number) { (void)signal_number; stopping = 1; }
+
+int main(void) {
+ if (setpgid(0,0) && errno != EACCES) return 1;
+ struct sigaction action = {0}; action.sa_handler = stop_now;
+ sigemptyset(&action.sa_mask); sigaction(SIGTERM,&action,NULL); sigaction(SIGINT,&action,NULL);
+ umask(0);
+ if (mkdir(SOCKET_DIR,0777) && errno != EEXIST) return 2;
+ chmod(SOCKET_DIR,0777); unlink(SOCKET_PATH);
+ int server=socket(AF_UNIX,SOCK_STREAM,0); if(server<0) return 3;
+ struct sockaddr_un address={0}; address.sun_family=AF_UNIX;
+ strncpy(address.sun_path,SOCKET_PATH,sizeof(address.sun_path)-1);
+ if(bind(server,(struct sockaddr *)&address,sizeof(address)) || chmod(SOCKET_PATH,0666) || listen(server,2)) return 4;
+ printf("camera broker ready uid=%d socket=%s\n",getuid(),SOCKET_PATH); fflush(stdout);
+ while(!stopping) {
+ int client=accept(server,NULL,NULL);
+ if(client<0) { if(errno==EINTR) continue; break; }
+ pid_t child=fork();
+ if(child==0) {
+ close(server);
+ fprintf(stderr,"camera worker accepted pid=%d\n",getpid()); fflush(stderr);
+ gid_t groups[]={1006};
+ if(setgroups(1,groups) || setgid(1006) || setuid(1006)) _exit(120);
+ if(dup2(client,STDIN_FILENO)<0 || dup2(client,STDOUT_FILENO)<0) _exit(121);
+ if(client>STDOUT_FILENO) close(client);
+ setenv("LD_LIBRARY_PATH","/system/lib:/data/local/tmp",1);
+ execl(WORKER,"vulpes-beatles-camera-worker",(char *)NULL);
+ _exit(127);
+ }
+ close(client);
+ while(waitpid(-1,NULL,WNOHANG)>0) {}
+ }
+ close(server); unlink(SOCKET_PATH); return 0;
+}
diff --git a/tundra/probes/beatles-camera-glconsumer-probe.cpp b/tundra/probes/beatles-camera-glconsumer-probe.cpp
new file mode 100644
index 00000000..d4499341
--- /dev/null
+++ b/tundra/probes/beatles-camera-glconsumer-probe.cpp
@@ -0,0 +1,175 @@
+// SPDX-License-Identifier: MPL-2.0
+// Bounded Android API 23 Camera1 diagnostic using an EGL/GLConsumer output path.
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifndef GL_TEXTURE_EXTERNAL_OES
+#define GL_TEXTURE_EXTERNAL_OES 0x8D65
+#endif
+
+static GLuint compileShader(GLenum type, const char* source) {
+ GLuint shader = glCreateShader(type);
+ glShaderSource(shader, 1, &source, nullptr);
+ glCompileShader(shader);
+ GLint ok = 0;
+ glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
+ if (!ok) {
+ char log[1024];
+ glGetShaderInfoLog(shader, sizeof(log), nullptr, log);
+ fprintf(stderr, "shader_error=%s\n", log);
+ return 0;
+ }
+ return shader;
+}
+
+int main() {
+ setvbuf(stderr, nullptr, _IONBF, 0);
+ android::ProcessState::self()->startThreadPool();
+ const int cameraCount = android::Camera::getNumberOfCameras();
+ fprintf(stderr, "camera_count=%d uid=%d\n", cameraCount, getuid());
+ if (cameraCount < 1) return 2;
+
+ android::sp camera = android::Camera::connect(
+ 0, android::String16("org.vulpes.camera.glprobe"),
+ android::Camera::USE_CALLING_UID);
+ if (camera == nullptr) {
+ fprintf(stderr, "camera_connect_failed\n");
+ return 3;
+ }
+ android::CameraParameters parameters(camera->getParameters());
+ int width = 0;
+ int height = 0;
+ parameters.getPreviewSize(&width, &height);
+ fprintf(stderr, "stock_preview_size=%dx%d format=%s\n", width, height,
+ parameters.getPreviewFormat());
+ if (width <= 0 || height <= 0 || width > 1920 || height > 1920) return 4;
+
+ EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ EGLint major = 0, minor = 0;
+ if (display == EGL_NO_DISPLAY || !eglInitialize(display, &major, &minor)) {
+ fprintf(stderr, "egl_initialize_error=%x\n", eglGetError());
+ return 5;
+ }
+ const EGLint configAttributes[] = {
+ EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
+ EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+ EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_NONE};
+ EGLConfig config;
+ EGLint configCount = 0;
+ if (!eglChooseConfig(display, configAttributes, &config, 1, &configCount) ||
+ configCount < 1) return 6;
+ const EGLint surfaceAttributes[] = {
+ EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE};
+ EGLSurface surface = eglCreatePbufferSurface(display, config, surfaceAttributes);
+ const EGLint contextAttributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
+ EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT,
+ contextAttributes);
+ if (surface == EGL_NO_SURFACE || context == EGL_NO_CONTEXT ||
+ !eglMakeCurrent(display, surface, surface, context)) {
+ fprintf(stderr, "egl_context_error=%x\n", eglGetError());
+ return 7;
+ }
+
+ GLuint texture = 0;
+ glGenTextures(1, &texture);
+ glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
+ glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+
+ android::sp producer;
+ android::sp consumer;
+ android::sp allocator =
+ new android::GraphicBufferAlloc();
+ android::BufferQueue::createBufferQueue(&producer, &consumer, allocator);
+ consumer->setDefaultBufferSize(width, height);
+ android::sp glConsumer = new android::GLConsumer(
+ consumer, texture, GL_TEXTURE_EXTERNAL_OES, true, false);
+
+ const int targetStatus = camera->setPreviewTarget(producer);
+ fprintf(stderr, "set_preview_target=%d\n", targetStatus);
+ const int startStatus = camera->startPreview();
+ fprintf(stderr, "start_preview=%d\n", startStatus);
+ if (targetStatus || startStatus) return 8;
+
+ const char* vertexSource =
+ "attribute vec4 a; attribute vec2 t; varying vec2 v;"
+ "void main(){gl_Position=a;v=t;}";
+ const char* fragmentSource =
+ "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float; uniform samplerExternalOES s; varying vec2 v;"
+ "void main(){gl_FragColor=texture2D(s,v);}";
+ GLuint program = glCreateProgram();
+ GLuint vertex = compileShader(GL_VERTEX_SHADER, vertexSource);
+ GLuint fragment = compileShader(GL_FRAGMENT_SHADER, fragmentSource);
+ if (!vertex || !fragment) return 9;
+ glAttachShader(program, vertex);
+ glAttachShader(program, fragment);
+ glBindAttribLocation(program, 0, "a");
+ glBindAttribLocation(program, 1, "t");
+ glLinkProgram(program);
+ GLint linked = 0;
+ glGetProgramiv(program, GL_LINK_STATUS, &linked);
+ if (!linked) {
+ char log[1024];
+ glGetProgramInfoLog(program, sizeof(log), nullptr, log);
+ fprintf(stderr, "program_link_error=%s\n", log);
+ return 10;
+ }
+ const GLfloat vertices[] = {-1, -1, 1, -1, -1, 1, 1, 1};
+ const GLfloat texcoords[] = {0, 1, 1, 1, 0, 0, 1, 0};
+ glUseProgram(program);
+ glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, vertices);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, texcoords);
+ glEnableVertexAttribArray(1);
+ glUniform1i(glGetUniformLocation(program, "s"), 0);
+
+ bool captured = false;
+ for (int attempt = 0; attempt < 100 && !captured; ++attempt) {
+ usleep(100000);
+ const int updateStatus = glConsumer->updateTexImage();
+ if (updateStatus != 0) continue;
+ glViewport(0, 0, width, height);
+ glClear(GL_COLOR_BUFFER_BIT);
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+ glFinish();
+ unsigned char* pixels = new unsigned char[width * height * 3];
+ glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, pixels);
+ const GLenum glError = glGetError();
+ if (glError == GL_NO_ERROR) {
+ FILE* output = fopen("/data/local/tmp/vulpes-camera-glconsumer.ppm", "wb");
+ if (output) {
+ fprintf(output, "P6\n%d %d\n255\n", width, height);
+ for (int y = height - 1; y >= 0; --y)
+ fwrite(pixels + y * width * 3, 3, width, output);
+ fclose(output);
+ chmod("/data/local/tmp/vulpes-camera-glconsumer.ppm", 0644);
+ captured = true;
+ }
+ }
+ delete[] pixels;
+ fprintf(stderr, "update=%d gl_error=%x captured=%d\n",
+ updateStatus, glError, captured);
+ }
+ fprintf(stderr, "stopping_preview\n");
+ camera->stopPreview();
+ fprintf(stderr, "disconnecting_camera\n");
+ camera->disconnect();
+ fprintf(stderr, "captured=%d\n", captured);
+ eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
+ eglDestroyContext(display, context);
+ eglDestroySurface(display, surface);
+ eglTerminate(display);
+ return captured ? 0 : 11;
+}
diff --git a/tundra/probes/beatles-camera-gonkqueue-probe.cpp b/tundra/probes/beatles-camera-gonkqueue-probe.cpp
new file mode 100644
index 00000000..62bd34c3
--- /dev/null
+++ b/tundra/probes/beatles-camera-gonkqueue-probe.cpp
@@ -0,0 +1,213 @@
+// SPDX-License-Identifier: MPL-2.0
+// RAM-only diagnostic reproducing Gecko's Android 6 GonkBufferQueue path.
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define ANDROID_VERSION 23
+#include "GonkBufferQueueLL.h"
+#include "GonkBufferQueueProducer.h"
+
+class CameraEvents : public android::CameraListener {
+ public:
+ void notify(int32_t type, int32_t ext1, int32_t ext2) override {
+ fprintf(stderr, "camera_notify=%d,%d,%d\n", type, ext1, ext2);
+ }
+ void postData(int32_t type, const android::sp& memory,
+ camera_frame_metadata_t*) override {
+ if (memory == nullptr) return;
+ const size_t size = memory->size();
+ const unsigned char* bytes = static_cast(memory->pointer());
+ unsigned int low = 255, high = 0;
+ uint64_t sum = 0;
+ const size_t ySize = size < 640u * 480u ? size : 640u * 480u;
+ for (size_t i = 0; i < ySize; ++i) {
+ if (bytes[i] < low) low = bytes[i];
+ if (bytes[i] > high) high = bytes[i];
+ sum += bytes[i];
+ }
+ fprintf(stderr, "camera_data=%d size=%zu y=%u..%u mean=%.2f\n",
+ type, size, low, high, ySize ? (double)sum / ySize : 0);
+ static bool saved = false;
+ if (type == CAMERA_MSG_COMPRESSED_IMAGE) {
+ FILE* jpeg = fopen("/data/local/tmp/vulpes-camera-callback.jpg", "wb");
+ if (jpeg) { fwrite(bytes, 1, size, jpeg); fclose(jpeg); chmod("/data/local/tmp/vulpes-camera-callback.jpg", 0644); }
+ fprintf(stderr, "captured_jpeg=%zu\n", size);
+ } else if (!saved) {
+ FILE* output = fopen("/data/local/tmp/vulpes-camera-callback.yuv", "wb");
+ if (output) { fwrite(bytes, 1, size, output); fclose(output); chmod("/data/local/tmp/vulpes-camera-callback.yuv", 0644); saved = true; }
+ }
+ }
+ void postDataTimestamp(nsecs_t, int32_t type,
+ const android::sp&) override {
+ fprintf(stderr, "camera_timestamp_data=%d\n", type);
+ }
+};
+
+class QueueListener : public android::ConsumerListener {
+ public:
+ void onFrameAvailable(const android::BufferItem&) override {}
+ void onFrameReplaced(const android::BufferItem&) override {}
+ void onBuffersReleased() override {}
+ void onSidebandStreamChanged() override {}
+};
+
+int main() {
+ setvbuf(stderr, nullptr, _IONBF, 0);
+ android::ProcessState::self()->startThreadPool();
+ const int count = android::Camera::getNumberOfCameras();
+ fprintf(stderr, "camera_count=%d uid=%d\n", count, getuid());
+ if (count < 1) return 2;
+
+ android::sp camera = android::Camera::connect(
+ // Match GonkCameraHwMgr.cpp exactly. CameraService/AppOps on this
+ // firmware treats the client package as part of camera authorization.
+ 0, android::String16("gonk.camera"),
+ android::Camera::USE_CALLING_UID);
+ if (camera == nullptr) return 3;
+ android::CameraParameters params(camera->getParameters());
+ android::Vector previews, pictures;
+ params.getSupportedPreviewSizes(previews);
+ params.getSupportedPictureSizes(pictures);
+ android::Size picture = {0, 0};
+ for (size_t i = 0; i < pictures.size(); ++i)
+ if (pictures[i].width * pictures[i].height > picture.width * picture.height)
+ picture = pictures[i];
+ android::Size preview = {0, 0};
+ uint64_t bestDelta = UINT64_MAX;
+ for (size_t i = 0; i < previews.size(); ++i) {
+ const android::Size candidate = previews[i];
+ if (picture.width && candidate.width * picture.height != picture.width * candidate.height)
+ continue;
+ const uint64_t area = (uint64_t)candidate.width * candidate.height;
+ const uint64_t delta = area > 640u * 480u ? area - 640u * 480u : 640u * 480u - area;
+ if (delta < bestDelta) { preview = candidate; bestDelta = delta; }
+ }
+ if (!preview.width && previews.size()) preview = previews[0];
+ if (!preview.width || !picture.width) {
+ fprintf(stderr, "no supported preview/picture size (preview=%zu picture=%zu)\n",
+ previews.size(), pictures.size());
+ camera->disconnect();
+ return 4;
+ }
+ params.setPreviewSize(preview.width, preview.height);
+ params.setPictureSize(picture.width, picture.height);
+ // Gecko applies its recording hint after selecting still/preview sizes and
+ // before starting the preview. Keep the probe's Camera1 state in step.
+ params.set("recording-hint", "false");
+ android::String8 configured = params.flatten();
+ android::status_t status = camera->setParameters(configured);
+ fprintf(stderr, "set_parameters=%d requested_preview=%dx%d picture=%dx%d\n",
+ status, preview.width, preview.height, picture.width, picture.height);
+ if (status != android::NO_ERROR) { camera->disconnect(); return 4; }
+ params.unflatten(camera->getParameters());
+ int width = 0, height = 0;
+ params.getPreviewSize(&width, &height);
+ int pictureWidth = 0, pictureHeight = 0;
+ params.getPictureSize(&pictureWidth, &pictureHeight);
+ fprintf(stderr, "preview=%dx%d picture=%dx%d format=%s\n", width, height,
+ pictureWidth, pictureHeight, params.getPreviewFormat());
+
+ android::sp producer;
+ android::sp consumer;
+ android::sp allocator =
+ new android::GraphicBufferAlloc();
+ android::GonkBufferQueue::createBufferQueue(&producer, &consumer, allocator);
+ status = static_cast(producer.get())
+ ->setSynchronousMode(false);
+ fprintf(stderr, "async_producer=%d\n", status);
+ status = consumer->setConsumerUsageBits(GRALLOC_USAGE_SW_READ_OFTEN);
+ fprintf(stderr, "cpu_read_usage=%d\n", status);
+ status = consumer->setMaxAcquiredBufferCount(4);
+ fprintf(stderr, "max_acquired=%d\n", status);
+ android::sp listener = new QueueListener();
+ android::sp proxy =
+ new android::GonkBufferQueue::ProxyConsumerListener(listener);
+ status = consumer->consumerConnect(proxy, false);
+ fprintf(stderr, "consumer_connect=%d\n", status);
+ android::sp cameraEvents = new CameraEvents();
+ camera->setListener(cameraEvents);
+ status = camera->setPreviewTarget(producer);
+ fprintf(stderr, "set_preview_target=%d\n", status);
+ camera->setPreviewCallbackFlags(CAMERA_FRAME_CALLBACK_FLAG_CAMERA);
+ fprintf(stderr, "preview_callback_flags=%d\n", CAMERA_FRAME_CALLBACK_FLAG_CAMERA);
+ const android::status_t focusEvents = camera->sendCommand(
+ CAMERA_CMD_ENABLE_FOCUS_MOVE_MSG, 1, 0);
+ fprintf(stderr, "focus_move_events=%d\n", focusEvents);
+ status = camera->startPreview();
+ fprintf(stderr, "start_preview=%d\n", status);
+ if (status) return 4;
+
+ unsigned acquired = 0;
+ bool saved = false;
+ const int64_t deadline = systemTime(SYSTEM_TIME_MONOTONIC) + 5000000000LL;
+ while (systemTime(SYSTEM_TIME_MONOTONIC) < deadline) {
+ android::IGonkGraphicBufferConsumer::BufferItem item;
+ status = consumer->acquireBuffer(&item, 0);
+ if (status != android::NO_ERROR) { usleep(20000); continue; }
+ ++acquired;
+ android::sp buffer = item.mGraphicBuffer;
+ if (item.mFence != nullptr) item.mFence->waitForever("vulpes-gonk-camera");
+ void* pixels = nullptr;
+ if (buffer != nullptr &&
+ buffer->lock(GRALLOC_USAGE_SW_READ_OFTEN, &pixels) == android::NO_ERROR &&
+ pixels != nullptr) {
+ const size_t bytes = (size_t)buffer->getStride() * buffer->getHeight() * 3 / 2;
+ const unsigned char* y = static_cast(pixels);
+ unsigned int minimum = 255, maximum = 0;
+ uint64_t sum = 0;
+ const size_t yBytes = (size_t)buffer->getStride() * buffer->getHeight();
+ for (size_t i = 0; i < yBytes; ++i) {
+ if (y[i] < minimum) minimum = y[i];
+ if (y[i] > maximum) maximum = y[i];
+ sum += y[i];
+ }
+ fprintf(stderr, "frame=%u number=%llu timestamp=%llu y=%u..%u mean=%.2f format=%d\n",
+ acquired, (unsigned long long)item.mFrameNumber, (unsigned long long)item.mTimestamp, minimum, maximum,
+ (double)sum / yBytes, buffer->format);
+ if (!saved || acquired % 10 == 0) {
+ FILE* output = fopen("/data/local/tmp/vulpes-camera-gonkqueue.nv21", "wb");
+ if (output) {
+ fwrite(pixels, 1, bytes, output);
+ fclose(output);
+ chmod("/data/local/tmp/vulpes-camera-gonkqueue.nv21", 0644);
+ FILE* meta = fopen("/data/local/tmp/vulpes-camera-gonkqueue.txt", "w");
+ if (meta) {
+ fprintf(meta, "%u %u %u %d %zu %u %u %u %u\n", buffer->getWidth(),
+ buffer->getHeight(), buffer->getStride(), buffer->format, bytes,
+ acquired, minimum, maximum, (unsigned)(sum / yBytes));
+ fclose(meta);
+ chmod("/data/local/tmp/vulpes-camera-gonkqueue.txt", 0644);
+ }
+ saved = true;
+ }
+ }
+ buffer->unlock();
+ }
+ consumer->releaseBuffer(item.mBuf, item.mFrameNumber, android::Fence::NO_FENCE);
+ }
+ fprintf(stderr, "acquired=%u saved=%d; taking_still\n", acquired, saved);
+ status = camera->takePicture(CAMERA_MSG_SHUTTER | CAMERA_MSG_COMPRESSED_IMAGE);
+ fprintf(stderr, "take_picture=%d\n", status);
+ sleep(3);
+ fprintf(stderr, "stopping_preview\n");
+ camera->stopPreview();
+ fprintf(stderr, "camera_stopped; disconnecting\n");
+ camera->disconnect();
+ consumer->consumerDisconnect();
+ fprintf(stderr, "done\n");
+ return saved ? 0 : 5;
+}
diff --git a/tundra/probes/beatles-camera-permission-bridge.cpp b/tundra/probes/beatles-camera-permission-bridge.cpp
new file mode 100644
index 00000000..37d7a368
--- /dev/null
+++ b/tundra/probes/beatles-camera-permission-bridge.cpp
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: MPL-2.0
+// Temporary, narrowly scoped API-23 permission controller for a RAM camera test.
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+const android::String16 kDescriptor("android.os.IPermissionController");
+const android::String16 kCameraPermission("android.permission.CAMERA");
+const android::String16 kSystemEventPermission("android.permission.CAMERA_SEND_SYSTEM_EVENTS");
+const android::String16 kProbePackage("gonk.camera");
+const android::String16 kSystemPackage("android");
+const int32_t kCameraUid = 1006;
+
+class CameraPermissionBridge : public android::BBinder {
+ public:
+ const android::String16& getInterfaceDescriptor() const override {
+ return kDescriptor;
+ }
+
+ protected:
+ android::status_t onTransact(uint32_t code, const android::Parcel& data,
+ android::Parcel* reply, uint32_t flags) override {
+ if (code == android::IBinder::FIRST_CALL_TRANSACTION) {
+ if (!data.enforceInterface(kDescriptor)) return android::PERMISSION_DENIED;
+ const android::String16 permission = data.readString16();
+ const int32_t pid = data.readInt32();
+ const int32_t uid = data.readInt32();
+ (void)pid;
+ const bool grant = (permission == kCameraPermission && uid == kCameraUid) ||
+ (permission == kSystemEventPermission && uid == 1000);
+ android::String8 permissionName(permission);
+ fprintf(stderr, "permission check uid=%d name=%s grant=%d\n", uid,
+ permissionName.string(), grant);
+ reply->writeNoException();
+ reply->writeInt32(grant ? 1 : 0);
+ return android::NO_ERROR;
+ }
+ if (code == android::IBinder::FIRST_CALL_TRANSACTION + 1) {
+ if (!data.enforceInterface(kDescriptor)) return android::PERMISSION_DENIED;
+ const int32_t uid = data.readInt32();
+ reply->writeNoException();
+ const bool hasPackage = uid == kCameraUid || uid == 1000;
+ reply->writeInt32(hasPackage ? 1 : 0);
+ if (uid == kCameraUid) reply->writeString16(kProbePackage);
+ else if (uid == 1000) reply->writeString16(kSystemPackage);
+ return android::NO_ERROR;
+ }
+ if (code == android::IBinder::FIRST_CALL_TRANSACTION + 2) {
+ if (!data.enforceInterface(kDescriptor)) return android::PERMISSION_DENIED;
+ const android::String16 permission = data.readString16();
+ reply->writeNoException();
+ reply->writeInt32(permission == kCameraPermission ? 1 : 0);
+ return android::NO_ERROR;
+ }
+ return android::BBinder::onTransact(code, data, reply, flags);
+ }
+};
+}
+
+int main(int argc, char**) {
+ if (argc != 1) return 2;
+ android::ProcessState::self()->startThreadPool();
+ android::sp manager = android::defaultServiceManager();
+ android::sp service = new CameraPermissionBridge();
+ android::status_t result = manager->addService(android::String16("permission"), service);
+ fprintf(stderr, "register permission bridge pid=%d uid=%d result=%d\n",
+ getpid(), getuid(), result);
+ if (result != android::NO_ERROR) return 3;
+ gid_t systemGroup = 1000;
+ if (setgroups(1, &systemGroup) || setgid(1000) || setuid(1000)) {
+ perror("drop permission bridge to Android system UID");
+ return 4;
+ }
+ fprintf(stderr, "permission bridge ready\n");
+ android::sp cameraBinder =
+ manager->checkService(android::String16("media.camera"));
+ android::sp cameraService =
+ android::interface_cast(cameraBinder);
+ if (cameraService != nullptr) {
+ const int32_t activeUsers[] = {0};
+ cameraService->notifySystemEvent(android::ICameraService::USER_SWITCHED,
+ activeUsers, 1);
+ fprintf(stderr, "notified CameraService active user 0\n");
+ } else {
+ fprintf(stderr, "CameraService unavailable for active-user notification\n");
+ }
+ android::IPCThreadState::self()->joinThreadPool();
+ return 0;
+}
diff --git a/tundra/probes/beatles-camera-service-probe.cpp b/tundra/probes/beatles-camera-service-probe.cpp
new file mode 100644
index 00000000..06ebc30e
--- /dev/null
+++ b/tundra/probes/beatles-camera-service-probe.cpp
@@ -0,0 +1,173 @@
+// SPDX-License-Identifier: MPL-2.0
+// Bounded diagnostic for the Android Camera1 service on Nokia 2720 (API 23).
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+class ProbeListener : public android::CameraListener {
+ public:
+ ProbeListener() : frames(0), bytes(0), jpegBytes(0), yMean(0), yMin(0), yMax(0) {}
+ void notify(int32_t type, int32_t first, int32_t second) override {
+ fprintf(stderr, "notify type=%d first=%d second=%d\n", type, first, second);
+ }
+ void postData(int32_t type, const android::sp& data,
+ camera_frame_metadata_t*) override {
+ if (data == nullptr) return;
+ if (type == CAMERA_MSG_PREVIEW_FRAME) {
+ android::Mutex::Autolock lock(mutex);
+ ++frames;
+ bytes = data->size();
+ const size_t yBytes = bytes < 640u * 480u ? bytes : 640u * 480u;
+ const unsigned char* y = static_cast(data->pointer());
+ uint64_t sum = 0;
+ yMin = 255;
+ yMax = 0;
+ for (size_t i = 0; i < yBytes; ++i) {
+ if (y[i] < yMin) yMin = y[i];
+ if (y[i] > yMax) yMax = y[i];
+ sum += y[i];
+ }
+ yMean = yBytes ? (double)sum / yBytes : 0;
+ if (frames == 20 && bytes) {
+ FILE* out = fopen("/data/local/tmp/vulpes-camera-frame20.nv21", "wb");
+ if (out) {
+ fwrite(data->pointer(), 1, bytes, out);
+ fclose(out);
+ chmod("/data/local/tmp/vulpes-camera-frame20.nv21", 0644);
+ }
+ }
+ } else if (type == CAMERA_MSG_COMPRESSED_IMAGE) {
+ FILE* out = fopen("/data/local/tmp/vulpes-camera-photo.jpg", "wb");
+ if (out) {
+ jpegBytes = fwrite(data->pointer(), 1, data->size(), out);
+ fclose(out);
+ chmod("/data/local/tmp/vulpes-camera-photo.jpg", 0644);
+ }
+ fprintf(stderr, "jpeg_callback_bytes=%zu\n", jpegBytes);
+ }
+ }
+ void postDataTimestamp(nsecs_t, int32_t,
+ const android::sp&) override {}
+ android::Mutex mutex;
+ unsigned frames;
+ size_t bytes;
+ size_t jpegBytes;
+ double yMean;
+ unsigned yMin;
+ unsigned yMax;
+};
+
+int main(int argc, char** argv) {
+ if (argc > 2) return 2;
+ setvbuf(stderr, nullptr, _IONBF, 0);
+ const int cameraId = argc == 2 ? atoi(argv[1]) : 0;
+ android::ProcessState::self()->startThreadPool();
+ const int count = android::Camera::getNumberOfCameras();
+ fprintf(stderr, "camera_count=%d uid=%d\n", count, getuid());
+ if (cameraId < 0 || cameraId >= count) return 3;
+ android::CameraInfo info;
+ int status = android::Camera::getCameraInfo(cameraId, &info);
+ fprintf(stderr, "camera_info_status=%d facing=%d orientation=%d\n",
+ status, info.facing, info.orientation);
+ android::sp camera = android::Camera::connect(
+ cameraId, android::String16("gonk.camera"),
+ android::Camera::USE_CALLING_UID);
+ if (camera == nullptr) {
+ fprintf(stderr, "camera_connect_failed\n");
+ return 4;
+ }
+ android::CameraParameters configured(camera->getParameters());
+ // Stock KaiOS Camera reports app.camera.givenPreviewSize as 320x240
+ // on the beatles device. Gecko then selects this 4:3 size against the
+ // 1600x1200 still capture size. Keep the probe on that exact path.
+ configured.setPreviewSize(320, 240);
+ configured.setPictureSize(1600, 1200);
+ configured.setPreviewFormat("yuv420sp");
+ configured.set("recording-hint", "false");
+ status = camera->setParameters(configured.flatten());
+ fprintf(stderr, "set_full_parameters=%d\n", status);
+ android::sp listener = new ProbeListener();
+ camera->setListener(listener);
+ camera->setPreviewCallbackFlags(CAMERA_FRAME_CALLBACK_FLAG_ENABLE_MASK);
+ android::String8 parameters = camera->getParameters();
+ fprintf(stderr, "camera_parameters=%s\n", parameters.string());
+ fprintf(stderr, "before_bufferqueue_create\n");
+ android::sp producer;
+ android::sp consumer;
+ android::sp allocator = new android::GraphicBufferAlloc();
+ android::BufferQueue::createBufferQueue(&producer, &consumer, allocator);
+ fprintf(stderr, "after_bufferqueue_create producer=%d consumer=%d\n", producer != nullptr, consumer != nullptr);
+ fprintf(stderr, "before_cpuconsumer\n");
+ android::sp previewConsumer =
+ new android::CpuConsumer(consumer, 4);
+ fprintf(stderr, "after_cpuconsumer\n");
+ status = consumer->setConsumerUsageBits(GRALLOC_USAGE_SW_READ_OFTEN |
+ GRALLOC_USAGE_HW_TEXTURE | GRALLOC_USAGE_HW_RENDER);
+ fprintf(stderr, "set_consumer_usage=%d bits=%08x\n", status,
+ (unsigned)(GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_HW_TEXTURE | GRALLOC_USAGE_HW_RENDER));
+ fprintf(stderr, "before_consumer_setname\n");
+ previewConsumer->setName(android::String8("Vulpes Camera Probe"));
+ fprintf(stderr, "after_consumer_setname\n");
+ fprintf(stderr, "before_consumer_size\n");
+ previewConsumer->setDefaultBufferSize(320, 240);
+ fprintf(stderr, "after_consumer_size\n");
+ fprintf(stderr, "before_set_preview_target\n");
+ status = camera->setPreviewTarget(producer);
+ fprintf(stderr, "set_preview_target=%d\n", status);
+ status = camera->startPreview();
+ fprintf(stderr, "start_preview=%d\n", status);
+ unsigned graphicFrames = 0, graphicSamples = 0;
+ unsigned long long graphicSum = 0;
+ unsigned graphicMin = 255, graphicMax = 0;
+ for (int elapsed = 0; elapsed < 3; elapsed++) {
+ android::CpuConsumer::LockedBuffer buffer;
+ while (previewConsumer->lockNextBuffer(&buffer) == android::NO_ERROR) {
+ ++graphicFrames;
+ fprintf(stderr, "graphic_frame=%u width=%u height=%u stride=%u format=%d\n",
+ graphicFrames, buffer.width, buffer.height, buffer.stride,
+ buffer.format);
+ if (buffer.data && graphicFrames >= 5) {
+ const size_t yBytes = (size_t)buffer.stride * buffer.height;
+ if (graphicFrames == 5) {
+ FILE* out = fopen("/data/local/tmp/vulpes-camera-graphic.nv21", "wb");
+ if (out) { fwrite(buffer.data, 1, yBytes * 3 / 2, out); fclose(out); chmod("/data/local/tmp/vulpes-camera-graphic.nv21", 0644); }
+ }
+ for (size_t i = 0; i < yBytes; i += 4096) {
+ unsigned value = buffer.data[i]; graphicSum += value; ++graphicSamples;
+ if (value < graphicMin) graphicMin = value;
+ if (value > graphicMax) graphicMax = value;
+ }
+ }
+ previewConsumer->unlockBuffer(buffer);
+ }
+ usleep(1000000);
+ }
+ {
+ android::Mutex::Autolock lock(listener->mutex);
+ fprintf(stderr, "preview_frames=%u frame_bytes=%zu callback_y_mean=%.2f min=%u max=%u graphic_frames=%u graphic_samples=%u graphic_y_mean=%.2f min=%u max=%u\n",
+ listener->frames, listener->bytes, listener->yMean, listener->yMin, listener->yMax,
+ graphicFrames, graphicSamples,
+ graphicSamples ? (double)graphicSum / graphicSamples : 0.0,
+ graphicMin, graphicMax);
+ }
+ std::thread([camera]() {
+ fprintf(stderr, "take_picture_begin\n");
+ const int pictureStatus = camera->takePicture(CAMERA_MSG_COMPRESSED_IMAGE);
+ fprintf(stderr, "take_picture=%d\n", pictureStatus);
+ }).detach();
+ sleep(4);
+ camera->stopPreview();
+ camera->disconnect();
+ return 0;
+}
diff --git a/tundra/probes/beatles-camera-worker.cpp b/tundra/probes/beatles-camera-worker.cpp
new file mode 100644
index 00000000..b250616f
--- /dev/null
+++ b/tundra/probes/beatles-camera-worker.cpp
@@ -0,0 +1,214 @@
+// SPDX-License-Identifier: MPL-2.0
+// CameraService worker for the Nokia 2720 Flip. The parent broker drops this
+// process to Android's camera UID before exec. Gaia talks to it over a local
+// socket; it never receives Binder or elevated credentials.
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+bool write_all(int fd, const void *data, size_t size) {
+ const char *p = static_cast(data);
+ while (size) {
+ ssize_t n = write(fd, p, size);
+ if (n <= 0) return false;
+ p += n; size -= static_cast(n);
+ }
+ return true;
+}
+bool read_line(int fd, std::string *line) {
+ line->clear();
+ char c;
+ while (line->size() < 2048) {
+ ssize_t n = read(fd, &c, 1);
+ if (n <= 0) return false;
+ if (c == '\n') return true;
+ line->push_back(c);
+ }
+ return false;
+}
+bool send_json(int fd, const std::string &json, bool frame=false,
+ uint32_t sequence=0, const std::vector *bytes=nullptr) {
+ uint32_t length = static_cast(json.size());
+ if (frame) {
+ uint32_t header[2] = {sequence, bytes ? static_cast(bytes->size()) : 0};
+ return write_all(fd, header, sizeof(header)) && (!bytes || write_all(fd,bytes->data(),bytes->size()));
+ }
+ return write_all(fd,&length,sizeof(length)) && write_all(fd,json.data(),json.size());
+}
+
+class CameraWorker : public android::CameraListener {
+ public:
+ explicit CameraWorker(int fd) : fd_(fd), camera_id_(0), sequence_(0), stopping_(false) {}
+ void notify(int32_t type, int32_t first, int32_t second) override {
+ fprintf(stderr,"camera notify type=%d first=%d second=%d\n",type,first,second);
+ }
+ void postData(int32_t type, const android::sp &data,
+ camera_frame_metadata_t *) override {
+ if (data == nullptr) return;
+ if (type == CAMERA_MSG_COMPRESSED_IMAGE) {
+ std::lock_guard lock(mutex_);
+ jpeg_.assign(static_cast(data->pointer()),
+ static_cast(data->pointer()) + data->size());
+ ready_.notify_all();
+ return;
+ }
+ if (type != CAMERA_MSG_PREVIEW_FRAME) return;
+ const size_t expected = 320u * 240u * 3u / 2u;
+ if (data->size() < expected) return;
+ {
+ std::lock_guard lock(mutex_);
+ frame_.assign(static_cast(data->pointer()),
+ static_cast(data->pointer()) + expected);
+ ++sequence_;
+ if(sequence_==1) fprintf(stderr,"camera first frame bytes=%zu uid=%d\n",frame_.size(),getuid());
+ }
+ ready_.notify_all();
+ }
+ void postDataTimestamp(nsecs_t, int32_t,
+ const android::sp &) override {}
+
+ int run() {
+ android::ProcessState::self()->startThreadPool();
+ std::string line;
+ while (read_line(fd_, &line)) {
+ if (line == "start" || line == "start 0") {
+ if (!start()) { send_json(fd_,"{\"ready\":false,\"error\":\"CAMERA_START_FAILED\"}"); continue; }
+ if (!send_json(fd_,"{\"ready\":true,\"width\":320,\"height\":240,\"format\":\"nv21\",\"orientation\":270,\"flashModes\":[\"off\",\"auto\",\"on\",\"torch\"],\"pictureSizes\":[{\"width\":1600,\"height\":1200}],\"focusModes\":[\"fixed\"],\"maxFocusAreas\":0,\"zoomRatios\":[1] }")) return 1;
+ } else if (line == "frame") {
+ std::vector frame;
+ uint32_t seq;
+ {
+ std::unique_lock lock(mutex_);
+ const uint32_t previous = delivered_;
+ ready_.wait_for(lock,std::chrono::seconds(2),[&]{return sequence_>previous || stopping_;});
+ if (sequence_<=previous || frame_.empty()) { if(!send_json(fd_,"",true,sequence_,nullptr)) return 1; continue; }
+ seq=sequence_; delivered_=seq; frame=frame_;
+ }
+ if (!send_json(fd_,"",true,seq,&frame)) return 1;
+ } else if (line == "stop") {
+ stop(); send_json(fd_,"{\"ok\":true}"); return 0;
+ } else if (line.find("\"action\":\"flash\"") != std::string::npos) {
+ const size_t key=line.find("\"mode\":\"");
+ const size_t begin=key==std::string::npos?key:key+8;
+ const size_t end=begin==std::string::npos?begin:line.find('"',begin);
+ if(begin==std::string::npos || end==std::string::npos) {
+ send_json(fd_,"{\"ok\":false,\"error\":\"CAMERA_INVALID_FLASH_MODE\"}");continue;
+ }
+ const std::string mode=line.substr(begin,end-begin);
+ android::CameraParameters params(camera_->getParameters());
+ params.set(android::CameraParameters::KEY_FLASH_MODE,mode.c_str());
+ const android::status_t status=camera_->setParameters(params.flatten());
+ if(status==android::NO_ERROR) {
+ camera_->setPreviewCallbackFlags(CAMERA_FRAME_CALLBACK_FLAG_CAMERA);
+ camera_->startPreview();
+ send_json(fd_,"{\"ok\":true,\"result\":true}");
+ }
+ else send_json(fd_,"{\"ok\":false,\"error\":\"CAMERA_FLASH_UNAVAILABLE\"}");
+ } else if (line.find("\"action\":\"zoom\"") != std::string::npos) {
+ // This module reports a single zoom ratio (1x), so expose a stable
+ // no-op instead of asking Camera1 to select an unsupported index.
+ send_json(fd_,"{\"ok\":true,\"result\":{\"zoom\":1}}");
+ } else if (line.find("\"action\":\"picture\"") != std::string::npos) {
+ { std::lock_guard lock(mutex_); jpeg_.clear(); }
+ const android::status_t status=camera_->takePicture(CAMERA_MSG_COMPRESSED_IMAGE);
+ std::vector jpeg;
+ {
+ std::unique_lock lock(mutex_);
+ ready_.wait_for(lock,std::chrono::seconds(12),[&]{return !jpeg_.empty() || stopping_;});
+ jpeg=jpeg_;
+ }
+ camera_->setPreviewCallbackFlags(CAMERA_FRAME_CALLBACK_FLAG_CAMERA);
+ camera_->startPreview();
+ if(status!=android::NO_ERROR || jpeg.empty()) {
+ send_json(fd_,"{\"ok\":false,\"error\":\"CAMERA_CAPTURE_FAILED\"}");continue;
+ }
+ char reply[128];
+ std::snprintf(reply,sizeof(reply),"{\"ok\":true,\"jpegLength\":%zu}",jpeg.size());
+ if(!send_json(fd_,reply) || !write_all(fd_,jpeg.data(),jpeg.size())) return 1;
+ } else {
+ send_json(fd_,"{\"ok\":false,\"error\":\"CAMERA_UNSUPPORTED_COMMAND\"}");
+ }
+ }
+ stop(); return 0;
+ }
+
+ private:
+ bool start() {
+ if (camera_.get() != nullptr) stop();
+ fprintf(stderr,"camera count=%d uid=%d\n",android::Camera::getNumberOfCameras(),getuid());
+ camera_ = android::Camera::connect(camera_id_,android::String16("gonk.camera"),android::Camera::USE_CALLING_UID);
+ if (camera_.get() == nullptr) {fprintf(stderr,"camera connect failed\n");return false;}
+ android::CameraParameters params(camera_->getParameters());
+ params.setPreviewSize(320,240); params.setPictureSize(1600,1200);
+ params.setPreviewFormat("yuv420sp"); params.set("recording-hint","false");
+ android::status_t status=camera_->setParameters(params.flatten());
+ fprintf(stderr,"camera set parameters=%d\n",status);
+ if (status != android::NO_ERROR) { stop(); return false; }
+ android::sp producer;
+ android::sp consumer;
+ android::sp allocator = new android::GraphicBufferAlloc();
+ android::BufferQueue::createBufferQueue(&producer,&consumer,allocator);
+ consumer->setConsumerUsageBits(GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_HW_TEXTURE | GRALLOC_USAGE_HW_RENDER);
+ consumer->setDefaultBufferSize(320,240);
+ consumer_ = new android::CpuConsumer(consumer,4);
+ consumer_->setName(android::String8("Vulpes Camera"));
+ status=camera_->setPreviewTarget(producer);
+ fprintf(stderr,"camera set target=%d\n",status);
+ if (status != android::NO_ERROR) { stop(); return false; }
+ camera_->setListener(this);
+ // The broker consumes copied Camera1 preview frames rather than video metadata.
+ camera_->setPreviewCallbackFlags(CAMERA_FRAME_CALLBACK_FLAG_CAMERA);
+ status=camera_->startPreview(); fprintf(stderr,"camera start preview=%d\n",status);
+ if (status != android::NO_ERROR) { stop(); return false; }
+ stopping_=false; delivered_=sequence_;
+ consumer_thread_=std::thread([this]{
+ while(!stopping_) {
+ android::CpuConsumer::LockedBuffer buffer;
+ if(consumer_->lockNextBuffer(&buffer)==android::NO_ERROR) consumer_->unlockBuffer(buffer);
+ else usleep(10000);
+ }
+ });
+ return true;
+ }
+ void stop() {
+ { std::lock_guard lock(mutex_); stopping_=true; }
+ ready_.notify_all();
+ if (camera_.get() != nullptr) { camera_->stopPreview(); camera_->disconnect(); camera_.clear(); }
+ if(consumer_thread_.joinable()) consumer_thread_.join();
+ consumer_.clear();
+ }
+ int fd_; int camera_id_;
+ android::sp camera_;
+ android::sp consumer_;
+ std::mutex mutex_; std::condition_variable ready_;
+ std::thread consumer_thread_;
+ std::vector frame_, jpeg_; uint32_t sequence_, delivered_; std::atomic stopping_;
+};
+}
+
+int main() {
+ setvbuf(stderr,nullptr,_IONBF,0);
+ android::sp worker = new CameraWorker(STDIN_FILENO);
+ return worker->run();
+}
diff --git a/tundra/probes/beatles-display-session.c b/tundra/probes/beatles-display-session.c
index e09ee846..08c5472b 100644
--- a/tundra/probes/beatles-display-session.c
+++ b/tundra/probes/beatles-display-session.c
@@ -31,6 +31,7 @@
#include "beatles-lid.h"
static volatile sig_atomic_t interrupted;
static int phone_controls;
+static int camera_broker_active;
static void interrupt(int sig) { interrupted = sig; }
static void inventory_camera_nodes(void) {
static const int minors[] = {0, 1, 32, 33};
@@ -238,6 +239,11 @@ static void inside(int seconds) {
require(mount("/proc", ROOT "/proc", NULL, MS_BIND, NULL), "bind proc");
require(mount("/sys", ROOT "/sys", NULL, MS_BIND, NULL), "bind sys");
require(mount(NULL, ROOT "/sys", NULL, MS_BIND | MS_REMOUNT | MS_RDONLY, NULL), "readonly sys");
+ if (camera_broker_active) {
+ if (mkdir(ROOT "/work/camera-ipc",0777) && errno!=EEXIST) _exit(1);
+ require(mount(BASE "/camera-ipc",ROOT "/work/camera-ipc",NULL,MS_BIND,NULL),"camera IPC mount");
+ require(chmod(ROOT "/work/camera-ipc",0777),"camera IPC permissions");
+ }
require(mount("tmpfs", ROOT "/dev", "tmpfs", MS_NOSUID, "size=1m,mode=0755"), "devices");
node(ROOT "/dev/null",1,3,0666); node(ROOT "/dev/zero",1,5,0666);
node(ROOT "/dev/random",1,8,0666); node(ROOT "/dev/urandom",1,9,0666);
@@ -261,6 +267,7 @@ static void inside(int seconds) {
setenv("VULPES_TRIAL_MANUAL",seconds >= 900 ? "1" : "0",1);
setenv("VULPES_TUNDRA_RADIO",phone_controls?"1":"0",1);
setenv("VULPES_TUNDRA_WIFI",phone_controls?"1":"0",1);
+ setenv("VULPES_TUNDRA_CAMERA",camera_broker_active?"1":"0",1);
setenv("LANG","C",1); setenv("DISPLAY",":0",1);
setenv("LD_LIBRARY_PATH","/usr/lib/arm-linux-gnueabihf:/lib/arm-linux-gnueabihf",1);
execl("/bin/sh","sh","/work/display-test.sh",NULL);
@@ -272,6 +279,7 @@ int main(int argc, char **argv) {
int seconds = !strcmp(argv[1],"--temporary-45s") ? 45 :
!strcmp(argv[1],"--temporary-90s") ? 90 :
(!strcmp(argv[1],"--temporary-180s") || !strcmp(argv[1],"--phone-180s")) ? 180 :
+ !strcmp(argv[1],"--temporary-240s") ? 240 :
(!strcmp(argv[1],"--interactive-900s") || !strcmp(argv[1],"--phone-900s")) ? 900 :
(!strcmp(argv[1],"--interactive-1800s") || !strcmp(argv[1],"--phone-1800s")) ? 1800 : 0;
if (!seconds) return 2;
@@ -309,6 +317,17 @@ int main(int argc, char **argv) {
* previous run's image be mistaken for a frame from this session. */
unlink(ROOT "/work/camera-frame.yuv");
unlink(ROOT "/work/camera-photo.jpg");
+ /* These names belong to our API-23 diagnostic; adb shell cannot remove
+ * camera-UID files from Android's sticky /data/local/tmp directory. */
+ unlink("/data/local/tmp/vulpes-camera-frame.nv21");
+ unlink("/data/local/tmp/vulpes-camera-frame20.nv21");
+ unlink("/data/local/tmp/vulpes-camera-graphic.nv21");
+ unlink("/data/local/tmp/vulpes-camera-photo.jpg");
+ unlink("/data/local/tmp/vulpes-camera-glconsumer.ppm");
+ unlink("/data/local/tmp/vulpes-camera-gonkqueue.nv21");
+ unlink("/data/local/tmp/vulpes-camera-gonkqueue.txt");
+ unlink("/data/local/tmp/vulpes-camera-callback.yuv");
+ unlink("/data/local/tmp/vulpes-camera-callback.jpg");
sleep(2);
if (!access(BASE "/camera-probe-once", F_OK)) {
unlink(BASE "/camera-probe-once");
@@ -326,12 +345,80 @@ int main(int argc, char **argv) {
}
int result = android_service("/system/bin/stop");
printf("stop b2g: %d\n",result);
- pid_t child = -1, radio = -1;
+ pid_t child = -1, radio = -1, permission_bridge = -1, camera_broker = -1;
struct lid_state lid;lid_init(&lid,fb);
if (!result) {
sleep(1);
puts("Camera node inventory after stopping B2G");
inventory_camera_nodes();
+ int gl_camera_probe = !access(BASE "/camera-gl-probe-once", F_OK);
+ int camera_service_probe = !access(BASE "/camera-service-probe-once", F_OK);
+ if (gl_camera_probe || camera_service_probe) {
+ unlink(BASE "/camera-service-probe-once");
+ unlink(BASE "/camera-gl-probe-once");
+ permission_bridge = fork();
+ if (!permission_bridge) {
+ int output = open(BASE "/camera-permission-bridge.log",
+ O_CREAT | O_TRUNC | O_WRONLY, 0644);
+ if (output >= 0) {
+ fchmod(output, 0644);
+ dup2(output, STDOUT_FILENO);
+ dup2(output, STDERR_FILENO);
+ close(output);
+ }
+ setenv("LD_LIBRARY_PATH", "/system/lib:/data/local/tmp", 1);
+ execl("/data/local/tmp/vulpes-camera-permission-bridge",
+ "vulpes-camera-permission-bridge", NULL);
+ _exit(127);
+ }
+ if (permission_bridge > 0) sleep(2);
+ pid_t probe = fork();
+ if (!probe) {
+ int output = open(gl_camera_probe ? BASE "/camera-gl-probe.log" :
+ BASE "/camera-service-probe.log",
+ O_CREAT | O_TRUNC | O_WRONLY, 0644);
+ if (output >= 0) {
+ fchmod(output, 0644);
+ dup2(output, STDOUT_FILENO);
+ dup2(output, STDERR_FILENO);
+ close(output);
+ }
+ gid_t groups[] = {1006};
+ if (setgroups(1, groups) || setgid(1006) || setuid(1006)) _exit(126);
+ setenv("LD_LIBRARY_PATH", "/system/lib:/data/local/tmp", 1);
+ if (gl_camera_probe)
+ execl("/data/local/tmp/vulpes-camera-gl-probe-20261023",
+ "vulpes-camera-gl-probe-20261023", NULL);
+ else
+ execl("/data/local/tmp/vulpes-camera-probe-20261023",
+ "vulpes-camera-probe-20261023", "0", NULL);
+ _exit(127);
+ }
+ if (probe > 0) {
+ int probe_status = 0, finished = 0;
+ for (int attempt = 0; attempt < (gl_camera_probe ? 200 : 100); attempt++) {
+ if (waitpid(probe, &probe_status, WNOHANG) == probe) {
+ finished = 1;
+ break;
+ }
+ usleep(100000);
+ }
+ if (!finished) {
+ kill(probe, SIGKILL);
+ waitpid(probe, &probe_status, 0);
+ printf("Camera%s probe timed out after %d seconds\n",
+ gl_camera_probe ? " EGL/GLConsumer" : "1", gl_camera_probe ? 20 : 10);
+ } else {
+ printf("CameraService probe exit: %d\n",
+ WIFEXITED(probe_status) ? WEXITSTATUS(probe_status) : -1);
+ }
+ }
+ if (permission_bridge > 0) {
+ kill(permission_bridge, SIGTERM);
+ waitpid(permission_bridge, NULL, 0);
+ permission_bridge = -1;
+ }
+ }
int preview_dump_probe = !access(BASE "/qcamera-preview-dump-once", F_OK);
if (!access(BASE "/qcamera-probe-once", F_OK) || preview_dump_probe) {
unlink(BASE "/qcamera-probe-once");
@@ -393,6 +480,47 @@ int main(int argc, char **argv) {
if (prop > 0) waitpid(prop, NULL, 0);
if (!umount(dump_path)) rmdir(dump_path);
}
+ camera_broker_active = !access(BASE "/camera-broker-once",F_OK);
+ if (camera_broker_active) {
+ unlink(BASE "/camera-broker-once");
+ if (mkdir(BASE "/camera-ipc",0777) && errno!=EEXIST) perror("camera IPC directory");
+ chmod(BASE "/camera-ipc",0777);
+ unlink(BASE "/camera-ipc/camera.sock");
+ permission_bridge=fork();
+ if(!permission_bridge) {
+ int output=open(BASE "/camera-permission-bridge.log",O_CREAT|O_TRUNC|O_WRONLY,0644);
+ if(output>=0){fchmod(output,0644);dup2(output,STDOUT_FILENO);dup2(output,STDERR_FILENO);close(output);}
+ setenv("LD_LIBRARY_PATH","/system/lib:/data/local/tmp",1);
+ execl("/data/local/tmp/vulpes-camera-permission-bridge","vulpes-camera-permission-bridge",NULL);
+ _exit(127);
+ }
+ if(permission_bridge>0) {
+ int ready=0;
+ for(int i=0;i<50;i++) {
+ FILE *log=fopen(BASE "/camera-permission-bridge.log","r");
+ if(log){char line[256];while(fgets(line,sizeof(line),log))if(strstr(line,"permission bridge ready"))ready=1;fclose(log);}
+ if(ready)break;
+ usleep(100000);
+ }
+ }
+ camera_broker=fork();
+ if(!camera_broker) {
+ int output=open(BASE "/camera-broker.log",O_CREAT|O_TRUNC|O_WRONLY,0644);
+ if(output>=0){fchmod(output,0644);dup2(output,STDOUT_FILENO);dup2(output,STDERR_FILENO);close(output);}
+ setenv("LD_LIBRARY_PATH","/system/lib:/data/local/tmp",1);
+ execl("/data/local/tmp/vulpes-beatles-camera-broker","vulpes-beatles-camera-broker",NULL);
+ _exit(127);
+ }
+ if(camera_broker>0) {
+ int ready=0;
+ for(int i=0;i<50;i++) {
+ struct stat info;
+ if(!stat(BASE "/camera-ipc/camera.sock",&info) && S_ISSOCK(info.st_mode)){ready=1;break;}
+ usleep(100000);
+ }
+ if(!ready){fprintf(stderr,"Camera broker did not create its socket\n");camera_broker_active=0;}
+ }
+ }
if (seconds >= 180) {
radio = fork();
if (!radio) radio_inside();
@@ -450,6 +578,18 @@ int main(int argc, char **argv) {
}
if(radio>0) {kill(-radio,SIGKILL);waitpid(radio,NULL,0);}
}
+ if (camera_broker > 0) {
+ kill(-camera_broker,SIGTERM);
+ for(int i=0;i<30;i++) {
+ if(waitpid(camera_broker,NULL,WNOHANG)==camera_broker){camera_broker=-1;break;}
+ usleep(100000);
+ }
+ if(camera_broker>0){kill(-camera_broker,SIGKILL);waitpid(camera_broker,NULL,0);}
+ }
+ if (permission_bridge > 0) {
+ kill(permission_bridge, SIGTERM);
+ waitpid(permission_bridge, NULL, 0);
+ }
clean_runtime();
lid_restore(&lid);
if (ioctl(fb,FBIOPUT_VSCREENINFO,&original)) perror("restore framebuffer mode");
diff --git a/tundra/runtime/beatles-camera-client.py b/tundra/runtime/beatles-camera-client.py
new file mode 100644
index 00000000..f0b1cb7c
--- /dev/null
+++ b/tundra/runtime/beatles-camera-client.py
@@ -0,0 +1,72 @@
+#!/usr/bin/python3.11
+# SPDX-License-Identifier: MPL-2.0
+"""Narrow stdio-to-socket adapter for the Android CameraService broker."""
+import os
+import json
+import socket
+import sys
+
+SOCKET = '/work/camera-ipc/camera.sock'
+
+def read_exact(stream, size):
+ data = bytearray()
+ while len(data) < size:
+ chunk = stream.recv(size - len(data))
+ if not chunk:
+ raise EOFError('Camera broker closed the session')
+ data.extend(chunk)
+ return bytes(data)
+
+def forward(sock, target, size):
+ header = read_exact(sock, size)
+ target.write(header); target.flush()
+ return header
+
+def packet(sock, output):
+ header = read_exact(sock, 4)
+ size = int.from_bytes(header, 'little')
+ if size > 65536:
+ raise ValueError('Camera broker sent an oversized response')
+ payload = read_exact(sock, size)
+ output.write(header + payload); output.flush()
+ return json.loads(payload.decode('utf-8'))
+
+def main():
+ client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ client.connect(SOCKET)
+ output = os.fdopen(os.dup(1), 'wb', buffering=0)
+ try:
+ client.sendall(b'start\n')
+ header = forward(client, output, 4)
+ length = int.from_bytes(header, 'little')
+ if not 0 < length <= 65536:
+ return 2
+ forward(client, output, length)
+ while True:
+ command = sys.stdin.buffer.readline(2049)
+ if not command or len(command) > 2048:
+ break
+ client.sendall(command)
+ if command.strip() == b'frame':
+ header = forward(client, output, 8)
+ length = int.from_bytes(header[4:], 'little')
+ if length > 320 * 240 * 3 // 2:
+ return 3
+ if length:
+ forward(client, output, length)
+ elif command.strip() == b'stop':
+ packet(client, output)
+ break
+ else:
+ response = packet(client, output)
+ jpeg = response.get('jpegLength', 0)
+ if isinstance(jpeg, int) and 0 < jpeg <= 32 * 1024 * 1024:
+ forward(client, output, jpeg)
+ except (BrokenPipeError, ConnectionError, EOFError):
+ return 0
+ finally:
+ client.close(); output.close()
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/tundra/runtime/buttons.py b/tundra/runtime/buttons.py
index 21502f45..fd343de6 100644
--- a/tundra/runtime/buttons.py
+++ b/tundra/runtime/buttons.py
@@ -86,6 +86,7 @@ def control(p):
wake_recovery.deadline = None
maximum = int((BACKLIGHT/'max_brightness').read_text())
(BACKLIGHT/'brightness').write_text(str(max(1, round(maximum * level)) if enabled else 0))
+ set_session_idle(not enabled)
elif p.get('operation') == 'vibrate':
vibration.start(p.get('pattern'))
elif p.get('operation') == 'volume':
@@ -103,6 +104,29 @@ def control(p):
raise ValueError('NOT_SUPPORTED')
return True
+def set_session_idle(idle):
+ """Tell logind that Gaia has switched the display between active and idle."""
+ try:
+ result = subprocess.run(['/usr/bin/loginctl', 'list-sessions', '--no-legend', '--no-pager'],
+ capture_output=True, text=True, timeout=3, check=True)
+ sessions = []
+ for line in result.stdout.splitlines():
+ fields = line.split()
+ if len(fields) >= 2 and fields[1] == '32011' and fields[0].isalnum():
+ sessions.append(fields[0])
+ if not sessions:
+ raise RuntimeError('No Tundra logind sessions found')
+ value = 'true' if idle else 'false'
+ for session in sessions:
+ subprocess.run(['/usr/bin/busctl', '--system', 'call', 'org.freedesktop.login1',
+ '/org/freedesktop/login1/session/'+session,
+ 'org.freedesktop.login1.Session', 'SetIdleHint', 'b', value],
+ capture_output=True, text=True, timeout=3, check=True)
+ return True
+ except (OSError, RuntimeError, subprocess.SubprocessError) as error:
+ print('TUNDRA_LOGIND_IDLE_FAILED '+str(error), file=sys.stderr, flush=True)
+ return False
+
def serve():
SOCKET.parent.mkdir(parents=True, exist_ok=True)
SOCKET.unlink(missing_ok=True)
diff --git a/tundra/runtime/camera-preview.py b/tundra/runtime/camera-preview.py
index f318cbe4..d26d34d9 100644
--- a/tundra/runtime/camera-preview.py
+++ b/tundra/runtime/camera-preview.py
@@ -28,9 +28,8 @@ class Listener(C.Structure):
class Camera:
- width, height = 640, 480
-
- def __init__(self, index):
+ def __init__(self, index, width=640, height=480):
+ self.width, self.height = width, height
self.camera = None
self.display = self.surface = self.context = None
self.frame = None
@@ -223,6 +222,8 @@ class Camera:
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--camera', type=int, choices=(0, 1), default=0)
+ parser.add_argument('--width', type=int, choices=(320, 640), default=640)
+ parser.add_argument('--height', type=int, choices=(240, 480), default=480)
parser.add_argument('--probe', action='store_true', help='Count frames for five seconds, retain no images')
args = parser.parse_args()
output = os.fdopen(os.dup(1), 'wb', buffering=0)
@@ -235,7 +236,9 @@ def main():
os.dup2(2, 1) # HAL callbacks can print; keep stdout protocol uncontaminated.
camera = None
try:
- camera = Camera(args.camera)
+ if (args.width, args.height) not in ((320,240),(640,480)):
+ raise ValueError('CAMERA_UNSUPPORTED_PREVIEW_SIZE')
+ camera = Camera(args.camera, args.width, args.height)
started = last_request = time.monotonic()
if not args.probe:
metadata = json.dumps(camera.metadata()).encode()
diff --git a/tundra/runtime/enable-cpuidle.py b/tundra/runtime/enable-cpuidle.py
new file mode 100644
index 00000000..87317168
--- /dev/null
+++ b/tundra/runtime/enable-cpuidle.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MPL-2.0
+"""Enable the Sargo Qualcomm low-power states when the kernel exposes them."""
+from pathlib import Path
+import time
+
+
+PARAMETER = Path('/sys/module/lpm_levels/parameters/sleep_disabled')
+
+
+def main():
+ deadline = time.monotonic() + 8
+ while time.monotonic() < deadline and not PARAMETER.exists():
+ time.sleep(.1)
+ if not PARAMETER.exists():
+ print('Qualcomm lpm_levels parameter unavailable; leaving kernel defaults')
+ return
+ current = PARAMETER.read_text().strip()
+ if current == 'N':
+ print('Qualcomm low-power states are already enabled')
+ return
+ if current != 'Y':
+ print('Unknown sleep_disabled value; leaving kernel setting unchanged:', current)
+ return
+ PARAMETER.write_text('N')
+ if PARAMETER.read_text().strip() != 'N':
+ raise RuntimeError('Could not enable Qualcomm CPU low-power states')
+ print('Enabled Qualcomm CPU low-power states')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tundra/runtime/hardware-status.py b/tundra/runtime/hardware-status.py
index 946da39f..23e650c5 100644
--- a/tundra/runtime/hardware-status.py
+++ b/tundra/runtime/hardware-status.py
@@ -15,7 +15,33 @@ def battery_state(root=Path('/sys/class/power_supply/battery')):
status = (root/'status').read_text().strip()
if not 0 <= level <= 100 or status not in ('Charging', 'Discharging', 'Full', 'Not charging'):
return None
- return {'level': level/100, 'charging': status in ('Charging', 'Full'), 'status': status}
+ charging = status in ('Charging', 'Full')
+ state = {'level': level/100, 'charging': charging, 'status': status}
+ # Linux power-supply values use matching micro-units, so their ratio
+ # gives hours. Keep the estimate absent when the driver has no usable
+ # current/energy telemetry; Gaia will show that honestly.
+ try:
+ energy = int((root/'energy_now').read_text().strip())
+ power = abs(int((root/'power_now').read_text().strip()))
+ if charging:
+ energy = int((root/'energy_full').read_text().strip()) - energy
+ if power > 0 and energy > 0:
+ seconds = energy * 3600 / power
+ if 0 < seconds < 10 * 24 * 3600:
+ state['chargingTime' if charging else 'dischargingTime'] = seconds
+ except (OSError, ValueError):
+ try:
+ charge = int((root/'charge_counter').read_text().strip())
+ current = abs(int((root/'current_now').read_text().strip()))
+ if charging:
+ charge = int((root/'charge_full').read_text().strip()) - charge
+ if current > 0 and charge > 0:
+ seconds = charge * 3600 / current
+ if 0 < seconds < 10 * 24 * 3600:
+ state['chargingTime' if charging else 'dischargingTime'] = seconds
+ except (OSError, ValueError):
+ pass
+ return state
except (OSError, ValueError):
return None
diff --git a/tundra/runtime/prepare-native.py b/tundra/runtime/prepare-native.py
index e3a814c8..e2eb9901 100644
--- a/tundra/runtime/prepare-native.py
+++ b/tundra/runtime/prepare-native.py
@@ -65,6 +65,18 @@ Description=Checkpoint Tundra clock once per minute
OnBootSec=60
OnUnitActiveSec=60
""")
+ write('etc/systemd/system/tundra-cpuidle.service', '''[Unit]
+Description=Enable Qualcomm CPU low-power states for Tundra
+After=systemd-modules-load.service tundra-hal.service
+Before=tundra-display.service
+[Service]
+Type=oneshot
+ExecStartPre=/usr/bin/python3 /etc/tundra/wait-hal.py
+ExecStart=/usr/bin/python3 /etc/tundra/enable-cpuidle.py
+RemainAfterExit=yes
+''')
+ shutil.copy2(Path(__file__).with_name('enable-cpuidle.py'),
+ root/'etc/tundra/enable-cpuidle.py')
wants = root/'etc/systemd/system/tundra.target.wants'
wants.mkdir(parents=True, exist_ok=True)
for unit in ('tundra-clock-restore.service', 'tundra-clock-save.timer'):
@@ -75,7 +87,7 @@ OnUnitActiveSec=60
write('etc/pam.d/other','auth required pam_deny.so\naccount required pam_deny.so\npassword required pam_deny.so\nsession required pam_deny.so\n')
write('etc/pam.d/common-account','account required pam_unix.so\n')
write('etc/pam.d/common-session-noninteractive','session required pam_unix.so\n')
- write('etc/systemd/logind.conf','[Login]\nHandlePowerKey=ignore\nIdleAction=ignore\n')
+ write('etc/systemd/logind.conf','[Login]\nHandlePowerKey=ignore\nIdleAction=suspend\nIdleActionSec=30s\n')
write('etc/systemd/journald.conf','[Journal]\nStorage=volatile\nRuntimeMaxUse=32M\n')
write('etc/passwd','root:x:0:0:root:/root:/bin/bash\ndroidian:x:32011:32011:Vulpes trial:/home/droidian:/bin/bash\nsystem:x:1000:1000:Android HAL:/:/usr/sbin/nologin\n')
write('etc/group','root:x:0:\ndroidian:x:32011:\nsystem:x:1000:droidian\ngraphics:x:1003:droidian\nandroid_input:x:1004:droidian\nvideo:x:44:droidian\nrender:x:105:droidian\ninput:x:104:droidian\nnetdev:x:988:\n')
@@ -111,8 +123,8 @@ TimeoutStopSec=15
write('etc/systemd/system/tundra-display.service','''[Unit]
Description=Gaia and Gecko on the Sargo display
Requires=tundra-hal.service dbus.service systemd-logind.service
-After=tundra-hal.service dbus.service systemd-logind.service systemd-user-sessions.service
-Wants=systemd-user-sessions.service
+Wants=tundra-cpuidle.service
+After=tundra-hal.service dbus.service systemd-logind.service tundra-cpuidle.service
[Service]
User=droidian
PAMName=tundra
@@ -122,6 +134,7 @@ Environment=XDG_SESSION_TYPE=wayland
Environment=XDG_CURRENT_DESKTOP=Vulpes
Environment=LANG=C.UTF-8
Environment=VULPES_TUNDRA=1
+Environment=VULPES_DEVICE=sargo
Environment=VULPES_TUNDRA_RADIO=1
Environment=WLR_BACKENDS=hwcomposer,libinput
Environment=EGL_PLATFORM=hwcomposer
@@ -144,11 +157,15 @@ TimeoutStopSec=15
display.write_text(display.read_text().replace('--own-compositor --hybris-egl',
'--own-compositor --hybris-egl --diagnostics'))
write('etc/tundra/wait-hal.py','''import subprocess,time
-for _ in range(120):
- p=subprocess.run(['/usr/bin/lxc-attach','-n','tundra-hal','--','/system/bin/getprop','init.svc.vendor.hwcomposer-2-2'],capture_output=True,text=True)
+deadline=time.monotonic()+8
+while time.monotonic().ext4')
parser.add_argument('--differential-ui', action='store_true', help='Bundle only UI files differing from the verified immutable system image')
parser.add_argument('--hold-hal',action='store_true',help='Boot basic.target first so HAL startup can be traced over USB')
@@ -45,6 +48,8 @@ def main():
parser.add_argument('--state', type=Path, help='Prepared Tundra data-image manifest; enables persistent writes in that file')
parser.add_argument('--no-deadline', action='store_true', help='Keep the persistent development boot running; requires --state')
args=parser.parse_args()
+ if not 60 <= args.watchdog_seconds <= 3600:
+ parser.error('--watchdog-seconds must be between 60 and 3600')
if args.hold_hal and not args.native:parser.error('--hold-hal requires --native')
if args.system_image and not args.native:parser.error('--system-image requires --native')
if args.differential_ui and not args.system_image:parser.error('--differential-ui requires --system-image')
@@ -79,6 +84,9 @@ def main():
else:
plan,_=load_boot().checked_plan()
storage_table = mount_table(layout)
+ if state and storage_table is None and not (
+ layout.get('schema') == 2 and layout.get('storage') == 'raw-userdata-ext4'):
+ raise ValueError('Persistent trials require inventoried ext4 userdata')
out=args.output.resolve();out.mkdir(parents=True,exist_ok=False)
source=Path(plan['backupDirectory'])/'boot_a.img'
packager=checked_source('aosp-mkbootimg')
@@ -86,6 +94,8 @@ def main():
result=subprocess.check_output(['python3',str(packager/'unpack_bootimg.py'),'--boot_img',str(source),'--out',str(unpack),'--format=mkbootimg'],text=True)
options=shlex.split(result);fields=dict(zip(options[::2],options[1::2]))
if fields['--header_version']!='0':raise ValueError('Expected header v0')
+ source_cmdline=fields['--cmdline']
+ fields['--cmdline'],removed_kernel_parameters=vulpes_cmdline(source_cmdline)
with tempfile.TemporaryDirectory() as temp:
helpers=select_helpers(read_newc((unpack/'ramdisk').read_bytes()),Path(temp),
extra=('sbin/blkid','sbin/e2fsck') if state else ())
@@ -113,15 +123,23 @@ def main():
(out/'known_hosts').write_text('[10.15.19.82]:2222 '+hostpublic+'\n[127.0.0.1]:2222 '+hostpublic+'\n')
init=(ROOT/'boot/rescue-init.sh').read_bytes().replace(b'# No LVM activation',
('EXPECTED_STORAGE_HASH='+shlex.quote(layout['firstMiBSha256'])+'\n# No LVM activation').encode())
- init=init.replace(b"'0 104448000 linear /dev/mmcblk0p72 329728'",
- shlex.quote(storage_table).encode())
- sources=[Path(__file__),ROOT/'boot/rescue-init.sh',ROOT/'tools/ramdisk_reference.py',
+ if storage_table is None:
+ uuid_hex=layout['filesystemUuid'].replace('-','')
+ init=init.replace(b'# No LVM activation',
+ ('RAW_USERDATA_EXT4=1\nEXPECTED_STORAGE_UUID_HEX='+uuid_hex+
+ '\n# No LVM activation').encode())
+ else:
+ init=init.replace(b"'0 104448000 linear /dev/mmcblk0p72 329728'",
+ shlex.quote(storage_table).encode())
+ sources=[Path(__file__),ROOT/'boot/rescue-init.sh',ROOT/'tools/kernel_cmdline.py',ROOT/'tools/ramdisk_reference.py',
ROOT/'tools/storage_layout.py']
if args.device_workspace:
sources += [ROOT/'tools/sargo_workspace.py', ROOT/'tools/boot_backup.py']
if state:
# Keep the existing layout intact; only /tundra/data/.ext4 is used
# for Tundra state. Opening its containing filesystem RW is necessary.
+ if storage_table is None:
+ init=init.replace(b'RAW_USERDATA_EXT4=1',b'RAW_USERDATA_EXT4=1\nTUNDRA_USERDATA_RW=1')
init=init.replace(b'--readonly --table', b'--table')
init=init.replace(b'-o ro,noload /dev/mapper/tundra-installed-ro', b'-o rw /dev/mapper/tundra-installed-ro')
init=init.replace(b'# Temporary RAM-only boot. Installed data is never mounted writable.', b'# Private persistent boot. Only the dedicated Tundra data file receives system writes.')
@@ -130,7 +148,7 @@ def main():
shared_ui = {}
if args.native:
binary=out/'watchdog'
- subprocess.run(['clang','--target=aarch64-linux-gnu','--ld-path='+str(linker()),'-Os','-ffreestanding','-fno-builtin','-fno-stack-protector','-fno-pie','-fno-unwind-tables','-fno-asynchronous-unwind-tables','-Wall','-Wextra','-Werror','-nostdlib','-static','-Wl,--build-id=none','-Wl,-e,_start',str(ROOT/'boot/watchdog.c'),'-o',str(binary)],check=True)
+ subprocess.run(['clang','--target=aarch64-linux-gnu','--ld-path='+str(linker()),'-Os','-ffreestanding','-fno-builtin','-fno-stack-protector','-fno-pie','-fno-unwind-tables','-fno-asynchronous-unwind-tables','-Wall','-Wextra','-Werror','-nostdlib','-static','-Wl,--build-id=none','-Wl,-e,_start','-DTUNDRA_WATCHDOG_SECONDS='+str(args.watchdog_seconds),str(ROOT/'boot/watchdog.c'),'-o',str(binary)],check=True)
init=init.replace(b"(sleep 480; echo 'Tundra trial deadline reached'; reboot -f) &",b'/sbin/tundra-watchdog &')
if args.no_deadline:
init=init.replace(b'/sbin/tundra-watchdog &', b'# Persistent development boot: no timed reboot.')
@@ -174,6 +192,8 @@ def main():
archive.add(config_dir, arcname=directory, filter=owned)
files['native-config.tar']=(0o600,compress_archive(buffer.getvalue()))
sources += list(sorted((ROOT/'runtime').glob('*.py')))
+ if args.runtime_config:
+ sources.append(ROOT/'tools/inspect-hardware.py')
files['sbin/tundra-watchdog']=(0o755,binary.read_bytes())
native_init=(ROOT/'boot/native-root.sh').read_bytes().replace(b'# BUILD_TIME_FLOOR', ('BUILD_EPOCH='+str(built_at)).encode())
if args.runtime_config:
@@ -181,7 +201,8 @@ def main():
if system:
sha = system['imageSha256']
native_init = native_init.replace(b'# A kernel fallback',
- ('SYSTEM_IMAGE=/installed/tundra/images/'+sha+'.ext4\nSYSTEM_IMAGE_SHA='+sha+'\n# A kernel fallback').encode())
+ ('SYSTEM_IMAGE=/installed/tundra/images/'+sha+'.ext4\nSYSTEM_IMAGE_SHA='+sha+
+ '\nSYSTEM_IMAGE_BYTES='+str(system['bytes'])+'\n# A kernel fallback').encode())
# Bundle UI changes with the boot trial rather than retransferring
# the entire immutable system image for each CSS/JS adjustment.
from shared_ui import build_payload, differential_payload
@@ -240,9 +261,10 @@ def main():
shutil.copy2(path,target)
save(out/'build.json',{'scope':'Private persistent Sargo development boot; unqualified' if state else ('Private temporary native-root trial; unqualified' if args.native else 'Private temporary SSH boot; installed root read-only; no graphical session'),
'builtAtUnix':built_at,'runtimeConfigOverlay':args.runtime_config,'networkTrial':args.network,'hardwareTrial':args.hardware,'interactiveDiagnostics':args.diagnostics,'image':image.name,'imageSha256':digest(image),'sourceBootSha256':digest(source),
+ 'removedKernelParameters':removed_kernel_parameters,
'serialSha256':plan['serialSha256'],'kernelSha256':kernel_hash,
'deviceWorkspaceSha256':plan.get('workspaceSha256'),
- 'ramdiskSha256':digest(ramdisk),'roundtripPassed':True,'flashReady':False,'localDebugger':args.local_debugger,'watchdogSeconds':None if args.no_deadline else (600 if args.native else 480),'persistentState':state,'storageWrites':bool(state),'nativeRootRequested':args.native,'halHeldForDiagnostics':args.hold_hal,
+ 'ramdiskSha256':digest(ramdisk),'roundtripPassed':True,'flashReady':False,'localDebugger':args.local_debugger,'watchdogSeconds':None if args.no_deadline else (args.watchdog_seconds if args.native else 480),'persistentState':state,'storageWrites':bool(state),'nativeRootRequested':args.native,'halHeldForDiagnostics':args.hold_hal,
'layout':layout,'layoutSha256':digest(args.layout),'systemImage':system,'sharedUiHashes':shared_ui,'sourceSnapshot':True,'sources':{str(p.relative_to(ROOT)):digest(p) for p in sources},
'helpers':{n:__import__('hashlib').sha256(b).hexdigest() for n,b in helpers.items()},
'sharedUiFilesAlreadyInSystem':omitted_ui if args.differential_ui else 0,
diff --git a/tundra/tools/build-state-image.py b/tundra/tools/build-state-image.py
index 9e5458bb..170ef232 100644
--- a/tundra/tools/build-state-image.py
+++ b/tundra/tools/build-state-image.py
@@ -5,6 +5,8 @@ import argparse
import json
import os
from pathlib import Path
+import shlex
+import shutil
import subprocess
import uuid
from persistent_state import FEATURES, digest
@@ -15,6 +17,8 @@ def main():
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--system-image', type=Path, required=True)
parser.add_argument('--size-mib', type=int, default=2048)
+ parser.add_argument('--profile-dir', type=Path,
+ help='Copy an existing droidian-trial browser profile into a fresh P23 state')
args = parser.parse_args()
if args.size_mib < 512 or args.size_mib > 8192:
parser.error('Choose 512–8192 MiB')
@@ -31,6 +35,23 @@ def main():
for directory in ('upper','work'):
(seed/directory).mkdir(mode=0o755 if directory == 'upper' else 0o700)
(seed/'upper').chmod(0o755)
+ migrated = []
+ opt_layer = None
+ if args.profile_dir:
+ profile = args.profile_dir.resolve(strict=True)
+ if not profile.is_dir() or profile.is_symlink():
+ raise ValueError('Profile source must be a real directory')
+ opt_layer = seed/'upper/opt'
+ vulpes_layer = opt_layer/'vulpes'
+ profiles_layer = vulpes_layer/'profiles'
+ profiles_layer.mkdir(parents=True)
+ opt_layer.chmod(0o755)
+ vulpes_layer.chmod(0o755)
+ profiles_layer.chmod(0o755)
+ target = profiles_layer/'droidian-trial'
+ target.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copytree(profile, target, symlinks=True)
+ migrated = [vulpes_layer, profiles_layer, target, *target.rglob('*')]
identifier = str(uuid.uuid4())
image = out/(identifier+'.ext4')
with image.open('xb') as stream:
@@ -42,12 +63,23 @@ def main():
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)
+ if migrated:
+ # Gaia runs as the droidian account (UID/GID 32011) on Sargo.
+ commands = out/'profile-ownership.debugfs'
+ commands.write_text(''.join(
+ f'set_inode_field {shlex.quote("/upper/" + str(path.relative_to(seed/"upper")))} {field} 32011\n'
+ for path in migrated for field in ('uid','gid')) +
+ 'set_inode_field /upper/opt uid 0\nset_inode_field /upper/opt gid 0\n')
+ subprocess.run(['debugfs','-w','-f',str(commands),str(image)],check=True,
+ stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
+ commands.unlink()
check = subprocess.run(['e2fsck','-f','-n',str(image)],text=True,capture_output=True)
(out/'filesystem-check.txt').write_text(check.stdout+check.stderr)
if check.returncode:raise RuntimeError('Data image failed filesystem verification')
manifest = {'schema':1,'device':'sargo','uuid':identifier,'image':image.name,
'imageSha256':digest(image),'bytes':image.stat().st_size,
'systemImageSha256':system['imageSha256'],'filesystemCheckPassed':True,
+ 'migratedProfile':bool(args.profile_dir),
'scope':'Private persistent overlay; containing filesystem retained; no partition formatting'}
(out/'state.json').write_text(json.dumps(manifest,indent=2)+'\n')
print(out/'state.json')
diff --git a/tundra/tools/inspect-hardware.py b/tundra/tools/inspect-hardware.py
index 1c7cac07..85de1256 100644
--- a/tundra/tools/inspect-hardware.py
+++ b/tundra/tools/inspect-hardware.py
@@ -2,6 +2,7 @@
# SPDX-License-Identifier: MPL-2.0
"""Read readiness, never dial, send messages, enter PINs or open camera nodes."""
import json
+import os
from pathlib import Path
import subprocess
import time
@@ -38,6 +39,150 @@ def selected(values, names):
if key in values and isinstance(values[key], dict)}
+def process_cpu_snapshot():
+ """Return process names and CPU ticks without command lines or arguments."""
+ result = {}
+ for entry in Path('/proc').iterdir():
+ if not entry.name.isdigit():
+ continue
+ try:
+ raw = (entry / 'stat').read_text()
+ # comm can contain spaces and parentheses; fields after the last )
+ # start at field 3 (state), so utime/stime are offsets 11 and 12.
+ fields = raw[raw.rfind(')') + 2:].split()
+ name = raw[raw.find('(') + 1:raw.rfind(')')]
+ result[int(entry.name)] = (name,
+ int(fields[11]) + int(fields[12]))
+ except (OSError, ValueError, IndexError):
+ continue
+ return result
+
+
+def thread_cpu_snapshot():
+ """Return thread names and CPU ticks; never collect command-line data."""
+ result = {}
+ for task in Path('/proc').glob('[0-9]*/task/[0-9]*'):
+ try:
+ raw = (task/'stat').read_text()
+ fields = raw[raw.rfind(')') + 2:].split()
+ name = raw[raw.find('(') + 1:raw.rfind(')')]
+ result[(int(task.parent.parent.name), int(task.name))] = (
+ name, int(fields[11]) + int(fields[12]))
+ except (OSError, ValueError, IndexError):
+ continue
+ return result
+
+
+def cpu_idle_snapshot():
+ """Read per-CPU idle-state residency counters where the kernel exports them."""
+ result = {}
+ for cpu in sorted(Path('/sys/devices/system/cpu').glob('cpu[0-9]*')):
+ if not cpu.name[3:].isdigit():
+ continue
+ states = []
+ for state in sorted((cpu/'cpuidle').glob('state[0-9]*')):
+ try:
+ states.append({key:(state/key).read_text().strip() for key in
+ ('name','desc','usage','time','disable')
+ if (state/key).is_file()})
+ except OSError:
+ continue
+ if states:
+ result[cpu.name] = states
+ return result
+
+
+def cpu_frequency_snapshot():
+ result = {}
+ for cpu in sorted(Path('/sys/devices/system/cpu').glob('cpu[0-9]*')):
+ if not cpu.name[3:].isdigit():
+ continue
+ path = cpu/'cpufreq'
+ values = {}
+ for key in ('scaling_governor','scaling_cur_freq','cpuinfo_cur_freq',
+ 'scaling_min_freq','scaling_max_freq'):
+ try:
+ values[key] = (path/key).read_text().strip()
+ except OSError:
+ pass
+ if values:
+ result[cpu.name] = values
+ return result
+
+
+def thermal_readings():
+ readings = {}
+ for zone in Path('/sys/class/thermal').glob('thermal_zone*'):
+ try:
+ label = (zone / 'type').read_text().strip()
+ value = int((zone / 'temp').read_text().strip())
+ # Linux thermal sysfs normally reports millidegrees Celsius.
+ readings[label or zone.name] = round(value / 1000, 1)
+ except (OSError, ValueError):
+ continue
+ return readings
+
+
+def power_supplies():
+ result = {}
+ for supply in Path('/sys/class/power_supply').glob('*'):
+ data = {}
+ for name in ('type', 'status', 'capacity', 'voltage_now',
+ 'current_now', 'power_now', 'temp', 'online'):
+ try:
+ data[name] = (supply / name).read_text().strip()
+ except OSError:
+ pass
+ if data:
+ result[supply.name] = data
+ return result
+
+
+def wakeup_snapshot():
+ """Read kernel wakeup counters; source labels are kernel driver names."""
+ for path in (Path('/sys/kernel/debug/wakeup_sources'),
+ Path('/sys/kernel/tracing/wakeup_sources')):
+ try:
+ lines = path.read_text().splitlines()
+ except OSError:
+ continue
+ if len(lines) < 2:
+ continue
+ headers = lines[0].split()
+ active_index = next((i for i, value in enumerate(headers)
+ if value.lower() == 'active_count'), None)
+ event_index = next((i for i, value in enumerate(headers)
+ if value.lower() == 'event_count'), None)
+ if active_index is None or event_index is None:
+ return None
+ result = {}
+ for line in lines[1:]:
+ fields = line.split()
+ try:
+ if len(fields) <= max(active_index, event_index):
+ continue
+ result[fields[0]] = {
+ 'activeCount':int(fields[active_index]),
+ 'eventCount':int(fields[event_index]),
+ }
+ except (ValueError, IndexError):
+ continue
+ return result
+ return None
+
+
+def counter_delta(before, after, keys):
+ """Return positive counter deltas, tolerating counters reset at reboot."""
+ result = {}
+ for name, values in (after or {}).items():
+ prior = (before or {}).get(name, {})
+ delta = {key:max(0, int(values[key])-int(prior.get(key, values[key])))
+ for key in keys if key in values}
+ if any(delta.values()):
+ result[name] = delta
+ return result
+
+
def inspect():
native = 'tundra.rescue=1' in Path('/proc/cmdline').read_text()
report = {'recordedAtUnix': time.time(), 'environment': 'tundra' if native else 'reference',
@@ -80,12 +225,114 @@ def inspect():
'applicationInstalled': Path('/usr/bin/droidian-camera').exists(),
'captureTested': False,
}
- report['battery'] = {}
- for name in ('capacity', 'status'):
+ report['battery'] = power_supplies()
+ report['thermalCelsius'] = thermal_readings()
+ backlights = {}
+ for path in Path('/sys/class/backlight').glob('*/brightness'):
try:
- report['battery'][name] = Path('/sys/class/power_supply/battery', name).read_text().strip()
+ backlights[path.parent.name] = (path.read_text().strip(),
+ (path.parent / 'max_brightness').read_text().strip())
+ except OSError:
+ continue
+ report['backlight'] = backlights
+ before = process_cpu_snapshot()
+ threads_before = thread_cpu_snapshot()
+ idle_before = cpu_idle_snapshot()
+ wakeups_before = wakeup_snapshot()
+ battery_before = power_supplies()
+ started = time.monotonic()
+ time.sleep(5)
+ elapsed = time.monotonic() - started
+ after = process_cpu_snapshot()
+ threads_after = thread_cpu_snapshot()
+ ticks_per_second = os.sysconf('SC_CLK_TCK')
+ cpu = []
+ for pid, (name, ticks) in after.items():
+ if pid not in before or before[pid][0] != name:
+ continue
+ delta = max(0, ticks - before[pid][1])
+ percent = 100 * delta / ticks_per_second / elapsed
+ if percent >= 1:
+ cpu.append({'process': name, 'percentOfOneCore': round(percent, 1)})
+ report['cpuSample'] = {'durationSeconds': round(elapsed, 2),
+ 'busyProcesses': sorted(cpu,
+ key=lambda item: item['percentOfOneCore'],
+ reverse=True)[:12]}
+ busy_threads = []
+ for key, (name, ticks) in threads_after.items():
+ prior = threads_before.get(key)
+ if prior is None or prior[0] != name:
+ continue
+ percent = 100 * max(0, ticks-prior[1]) / ticks_per_second / elapsed
+ if percent >= 1:
+ busy_threads.append({'processId':key[0], 'threadId':key[1],
+ 'thread':name,
+ 'percentOfOneCore':round(percent,1)})
+ report['cpuSample']['busyThreads'] = sorted(
+ busy_threads, key=lambda item:item['percentOfOneCore'], reverse=True)[:20]
+ idle_after = cpu_idle_snapshot()
+ residency = {}
+ for cpu_name, states in idle_after.items():
+ previous = idle_before.get(cpu_name, [])
+ before_by_name = {state.get('name'):state for state in previous}
+ deltas = []
+ for state in states:
+ prior = before_by_name.get(state.get('name'), {})
+ item = {'name':state.get('name')}
+ for key in ('usage','time'):
+ if key in state:
+ try:
+ item[key+'Delta'] = max(0,int(state[key])-int(prior.get(key,state[key])))
+ except ValueError:
+ pass
+ if 'disable' in state:
+ item['disabled'] = state['disable']
+ deltas.append(item)
+ if deltas:
+ residency[cpu_name] = deltas
+ report['cpuIdle'] = {'available':bool(residency),
+ 'statesDuringSample':residency,
+ 'frequencyAfterSample':cpu_frequency_snapshot(),
+ 'kernelCommandLineFlags':[
+ token for token in Path('/proc/cmdline').read_text().split()
+ if token.startswith(('lpm_levels.','cpuidle.','nohz='))],
+ 'lowPowerParameter':None,
+ 'systemSuspend':{}}
+ for key, path in {
+ 'sleepDisabled':'/sys/module/lpm_levels/parameters/sleep_disabled',
+ 'cpuIdleEnabled':'/sys/module/lpm_levels/parameters/enable_lpm',
+ 'powerState':'/sys/power/state',
+ 'memorySleep':'/sys/power/mem_sleep',
+ }.items():
+ try:
+ value = Path(path).read_text().strip()
+ if key in ('powerState','memorySleep'):
+ report['cpuIdle']['systemSuspend'][key] = value
+ else:
+ report['cpuIdle']['lowPowerParameter'] = {
+ **(report['cpuIdle']['lowPowerParameter'] or {}), key:value}
except OSError:
pass
+ suspend_stats = {}
+ for path in Path('/sys/power/suspend_stats').glob('*'):
+ if path.is_file():
+ try:
+ suspend_stats[path.name] = path.read_text().strip()
+ except OSError:
+ pass
+ report['cpuIdle']['systemSuspend']['statistics'] = suspend_stats or None
+ wakeups_after = wakeup_snapshot()
+ report['wakeupSources'] = {
+ 'available':wakeups_after is not None,
+ 'eventDeltas':counter_delta(wakeups_before,wakeups_after,('eventCount',)),
+ }
+ report['batteryDelta'] = {}
+ for supply, values in power_supplies().items():
+ prior = battery_before.get(supply,{})
+ for key in ('current_now','voltage_now','power_now','capacity','status'):
+ if key in values:
+ report['batteryDelta'].setdefault(supply,{})[key] = {
+ 'before':prior.get(key), 'after':values[key]}
return report
diff --git a/tundra/tools/kernel_cmdline.py b/tundra/tools/kernel_cmdline.py
new file mode 100644
index 00000000..e8e2c528
--- /dev/null
+++ b/tundra/tools/kernel_cmdline.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MPL-2.0
+"""Small helpers for keeping inherited Android boot flags out of Vulpes."""
+
+
+def remove_parameters(cmdline, names):
+ """Return (cleaned_cmdline, removed_tokens) for exact kernel parameter names."""
+ names = set(names)
+ kept = []
+ removed = []
+ for token in cmdline.split():
+ if token.split('=', 1)[0] in names:
+ removed.append(token)
+ else:
+ kept.append(token)
+ return ' '.join(kept), removed
+
+
+def vulpes_cmdline(cmdline):
+ # This Qualcomm cpuidle parameter disables every selectable low-power
+ # level. It is inherited from Android and must not disable Vulpes CPU idle.
+ return remove_parameters(cmdline, {'lpm_levels.sleep_disabled'})
diff --git a/tundra/tools/package-beatles-build.py b/tundra/tools/package-beatles-build.py
index 5b0d0823..c428f82a 100644
--- a/tundra/tools/package-beatles-build.py
+++ b/tundra/tools/package-beatles-build.py
@@ -52,6 +52,7 @@ def main():
files['opt/firefox/vulpes/tests/BeatlesAudit.sys.mjs']=ROOT/'tundra/devices/beatles/gaia-audit.sys.mjs'
files['opt/firefox/vulpes/tests/ProbeChild.sys.mjs']=ROOT/'tests/ProbeChild.sys.mjs'
files['opt/vulpes/assets/webapps/settings.gaiamobile.org/js/modules/wifi_utils.js']=ROOT/'assets/webapps/settings.gaiamobile.org/js/modules/wifi_utils.js'
+ files['opt/vulpes/tundra/runtime/beatles-camera-client.py']=ROOT/'tundra/runtime/beatles-camera-client.py'
add_beatles_t9(files)
for name in ('gaia-session.py','cover-display.py','radio_control.py','radio_status.py','call_audio.py','stream_volume.py','hardware_control.py','wifi_control.py','sms_pdu.py'):
files['work/'+name]=ROOT/'tundra/devices/beatles'/name
diff --git a/tundra/tools/refresh-boot-ui.py b/tundra/tools/refresh-boot-ui.py
index c3e7af2b..30d2a3b1 100644
--- a/tundra/tools/refresh-boot-ui.py
+++ b/tundra/tools/refresh-boot-ui.py
@@ -17,6 +17,7 @@ import io
import tarfile
from ramdisk_reference import read_newc
from shared_ui import build_payload, compress_archive
+from kernel_cmdline import vulpes_cmdline
from tundra import ROOT, checked_source, cpio, digest, save
@@ -46,6 +47,8 @@ def main():
options=shlex.split(text);fields=dict(zip(options[::2],options[1::2]))
if fields['--header_version']!='0' or digest(temp/'base/kernel')!=build['kernelSha256']:
raise ValueError('Unexpected boot format or kernel')
+ source_cmdline=fields['--cmdline']
+ fields['--cmdline'],removed_kernel_parameters=vulpes_cmdline(source_cmdline)
original=read_newc((temp/'base/ramdisk').read_bytes())
if 'shared-ui.tar' not in original or 'standalone-mount.sh' not in original:
raise ValueError('Missing standalone UI overlay')
@@ -112,12 +115,13 @@ def main():
for name in hashes:
target=out/'sources'/name;target.parent.mkdir(parents=True,exist_ok=True)
shutil.copy2(ROOT.parent/name,target)
- for path in (Path(__file__).resolve(),ROOT/'tools/shared_ui.py',*native_sources.values(),*([ROOT/'runtime/prepare-native.py'] if args.settings_adapters else [])):
+ for path in (Path(__file__).resolve(),ROOT/'tools/kernel_cmdline.py',ROOT/'tools/shared_ui.py',*native_sources.values(),*([ROOT/'runtime/prepare-native.py'] if args.settings_adapters else [])):
target=out/'sources'/path.relative_to(ROOT.parent)
target.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(path,target)
build['sources'][str(path.relative_to(ROOT))]=digest(path)
shutil.copy2(base/'client-key',out/'client-key')
build.update(image='boot.img',imageSha256=digest(out/'boot.img'),ramdiskSha256=digest(ramdisk),
+ removedKernelParameters=removed_kernel_parameters,
sharedUiHashes=hashes,builtAtUnix=int(time.time()),flashReady=False,
baseImageSha256=args.expected_sha256,uiOnlyRefresh=not args.settings_adapters,changedRamdiskEntries=changed,
scope='Private RAM Settings update; kernel and storage configuration preserved')
diff --git a/tundra/tools/run-beatles-gaia.py b/tundra/tools/run-beatles-gaia.py
index 1bd28a8c..e1762380 100644
--- a/tundra/tools/run-beatles-gaia.py
+++ b/tundra/tools/run-beatles-gaia.py
@@ -2,6 +2,7 @@
# SPDX-License-Identifier: MPL-2.0
"""Synchronize the shared host and run a bounded private Nokia Gaia trial."""
import argparse
+import hashlib
import json
from pathlib import Path
import shutil
@@ -15,6 +16,26 @@ OUT=ROOT/'tundra/out/beatles/gecko-156.0.1'
REMOTE='/data/local/vulpes-beatles'
+def compile_if_changed(command, output, sources, signature):
+ """Avoid relinking the same ARM tools on every bounded trial."""
+ stamp = output.with_name(output.name + '.build.json')
+ inputs = []
+ for path in sources:
+ stat = path.stat()
+ digest = hashlib.sha256(path.read_bytes()).hexdigest()
+ inputs.append({'path':str(path), 'size':stat.st_size, 'sha256':digest})
+ record = {'command':command,'signature':signature, 'inputs':inputs}
+ if output.is_file() and stamp.is_file():
+ try:
+ if json.loads(stamp.read_text()) == record:
+ print('Using cached ARM tool:', output.name, flush=True)
+ return
+ except (OSError, ValueError):
+ pass
+ subprocess.run(command, check=True)
+ stamp.write_text(json.dumps(record, sort_keys=True)+'\n')
+
+
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--interactive',action='store_true',help='Manual session, no automatic keypad actions; KaiOS returns within 30 minutes')
@@ -23,17 +44,46 @@ def main():
parser.add_argument('--phone',action='store_true',help='Enable Nokia radio, Wi-Fi, SMS/call and hardware control brokers')
parser.add_argument('--camera-probe',action='store_true',help='Run the bounded Qualcomm HAL preview probe before Gaia starts')
parser.add_argument('--camera-preview-binary',type=Path,help='Use a private QCamera preview-dump binary for a bounded RAM-only trial')
+ parser.add_argument('--camera-service-probe',action='store_true',help='Test Android Camera1/CameraService as the camera system UID in a bounded RAM-only trial')
+ parser.add_argument('--camera-service-probe-binary',type=Path,help='Private Android API 23 Camera1 diagnostic binary')
+ parser.add_argument('--camera-gl-probe',action='store_true',help='Test Camera1 through EGL/GLConsumer in a bounded RAM-only trial')
+ parser.add_argument('--camera-gl-probe-binary',type=Path,help='Private Android API 23 EGL/GLConsumer diagnostic binary')
+ parser.add_argument('--camera-permission-bridge-binary',type=Path,help='Temporary camera-only Android API 23 permission controller')
+ parser.add_argument('--camera-permission-bridge-runtime',type=Path,help='Android API 23 C++ runtime required by the permission bridge')
+ parser.add_argument('--camera-broker',action='store_true',help='Run the fixed-command CameraService bridge under UID 1006 during Gaia qualification')
+ parser.add_argument('--no-camera-broker',action='store_true',help='Disable the CameraService bridge for a non-camera session')
+ parser.add_argument('--camera-broker-worker',type=Path,help='Private Android API 23 persistent Camera1 worker binary')
+ parser.add_argument('--camera-broker-binary',type=Path,help='Private Android API 23 local camera broker binary')
+ parser.add_argument('--display-session-binary',type=Path,help='Use a prebuilt compatible session supervisor')
args=parser.parse_args()
- session_option=(('--phone-' if args.phone else '--interactive-')+str(args.minutes*60)+'s') if args.interactive else ('--phone-180s' if args.phone else '--temporary-90s' if (args.camera_probe or args.camera_preview_binary) else '--temporary-180s')
+ camera_diagnostics=(args.camera_probe, args.camera_preview_binary is not None,
+ args.camera_service_probe, args.camera_gl_probe)
+ camera_broker=args.camera_broker or (not args.no_camera_broker and not any(camera_diagnostics))
+ camera_modes=sum(camera_diagnostics)+(args.camera_broker is True)
+ if camera_modes > 1:
+ parser.error('Choose one camera probe per RAM trial; all use the same sensor')
+ session_option=(('--phone-' if args.phone else '--interactive-')+str(args.minutes*60)+'s') if args.interactive else ('--phone-180s' if args.phone else '--temporary-90s' if (args.camera_probe or args.camera_preview_binary or args.camera_service_probe or args.camera_gl_probe) else '--temporary-240s')
rows=subprocess.check_output(['adb','devices','-l'],text=True).splitlines()
devices=[r.split()[0] for r in rows if 'model:Nokia_2720_Flip' in r.split() and r.split()[1]=='device']
if len(devices)!=1:raise RuntimeError('Exactly one connected Nokia required')
adb=['adb','-s',devices[0]]
if subprocess.check_output(adb+['shell','getprop ro.product.codename'],text=True).strip()!='beatles':
raise RuntimeError('Unexpected device')
+ # A previous interrupted run must never make an old frame or log appear
+ # to belong to this trial. These paths are private diagnostics only.
+ stale = [REMOTE+'/camera-service-probe-once', REMOTE+'/camera-service-probe.log',
+ REMOTE+'/camera-gl-probe-once', REMOTE+'/camera-gl-probe.log',
+ REMOTE+'/camera-permission-bridge.log', REMOTE+'/qcamera-probe-once',
+ REMOTE+'/camera-broker-once',REMOTE+'/camera-broker.log',REMOTE+'/camera-ipc/camera.sock',
+ REMOTE+'/qcamera-probe.log', REMOTE+'/qcamera-preview-dump-once',
+ REMOTE+'/qcamera-preview-dump', REMOTE+'/rootfs/work/camera-frame.yuv',
+ REMOTE+'/rootfs/work/camera-photo.jpg']
+ cleanup=subprocess.run(adb+['shell','rm -f '+' '.join(stale)],capture_output=True,text=True)
+ if cleanup.returncode:
+ print('Could not clear every prior camera artifact:',cleanup.stderr.strip(),flush=True)
if not args.interactive:
lid=subprocess.check_output(adb+['shell','cat /sys/devices/soc.0/hall_sensor1.73/hall_status'],text=True).strip()
- if lid=='1' and not (args.camera_probe or args.camera_preview_binary):
+ if lid=='1' and not (args.camera_probe or args.camera_preview_binary or args.camera_service_probe or args.camera_gl_probe):
raise RuntimeError('Open the Nokia flip before running keypad qualification; no session started')
ini=subprocess.check_output(adb+['shell','cat '+REMOTE+'/rootfs/opt/firefox/application.ini'],text=True)
if 'Version=156.0.1' not in ini.splitlines():raise RuntimeError('Stage Gecko 156.0.1 before launching')
@@ -45,12 +95,25 @@ def main():
else:
subprocess.run(adb+['shell','rm -f '+REMOTE+'/rootfs/work/cover-animation.rgb565'],check=True,capture_output=True)
sysroot=OUT/'sysroot/rootfs'
- compiler=[str(OUT/'mozbuild/clang/bin/clang'),'--target=arm-linux-gnueabihf',
- '--sysroot='+str(sysroot),'--gcc-install-dir='+str(sysroot/'usr/lib/gcc/arm-linux-gnueabihf/12'),
- '-fuse-ld=lld','-march=armv7-a','-mfpu=neon','-mfloat-abi=hard','-O2','-Wall','-Wextra','-Werror']
- subprocess.run(compiler+['-static',str(ROOT/'tundra/probes/beatles-control-client.c'),'-o',str(OUT/'control-client')],check=True)
- subprocess.run(compiler+['-static',str(ROOT/'tundra/probes/beatles-display-session.c'),'-o',str(OUT/'display-session')],check=True)
- subprocess.run(compiler+[str(ROOT/'tundra/probes/beatles-keypad.c'),'-lX11','-lXtst','-o',str(OUT/'keypad')],check=True)
+ display_session=OUT/'display-session'
+ if args.display_session_binary:
+ if not args.display_session_binary.is_file():raise RuntimeError('Prebuilt session supervisor not found')
+ display_session=args.display_session_binary
+ else:
+ compiler=[str(OUT/'mozbuild/clang/bin/clang'),'--target=arm-linux-gnueabihf',
+ '--sysroot='+str(sysroot),'--gcc-install-dir='+str(sysroot/'usr/lib/gcc/arm-linux-gnueabihf/12'),
+ '-fuse-ld=lld','-march=armv7-a','-mfpu=neon','-mfloat-abi=hard','-O2','-Wall','-Wextra','-Werror']
+ compiler_stat=Path(compiler[0]).stat()
+ runtime_lock=sysroot.parent/'runtime-lock.json'
+ signature={'command':compiler,'compilerSize':compiler_stat.st_size,
+ 'compilerMtimeNs':compiler_stat.st_mtime_ns,
+ 'runtimeLockSha256':hashlib.sha256(runtime_lock.read_bytes()).hexdigest()}
+ compile_if_changed(compiler+['-static',str(ROOT/'tundra/probes/beatles-control-client.c'),'-o',str(OUT/'control-client')],
+ OUT/'control-client',[ROOT/'tundra/probes/beatles-control-client.c'],signature)
+ compile_if_changed(compiler+['-static',str(ROOT/'tundra/probes/beatles-display-session.c'),'-o',str(OUT/'display-session')],
+ OUT/'display-session',[ROOT/'tundra/probes/beatles-display-session.c'],signature)
+ compile_if_changed(compiler+[str(ROOT/'tundra/probes/beatles-keypad.c'),'-lX11','-lXtst','-o',str(OUT/'keypad')],
+ OUT/'keypad',[ROOT/'tundra/probes/beatles-keypad.c'],signature)
(OUT/'gaia-test.sh').write_text('exec /usr/bin/python3.11 /work/gaia-session.py\n')
dirs=' '.join(REMOTE+'/rootfs/'+d for d in ('opt/firefox/vulpes/tests','opt/firefox/defaults/pref','work','proc','sys','dev','tmp','run'))
subprocess.run(adb+['shell','mkdir -p '+dirs+'; chmod 1777 '+REMOTE+'/rootfs/tmp; rm -f '+REMOTE+'/display-session.done '+REMOTE+'/rootfs/work/gaia-audit.json'],capture_output=True,check=True)
@@ -65,6 +128,57 @@ def main():
subprocess.run(adb+['shell','rm -f '+REMOTE+'/qcamera-preview-dump '+REMOTE+'/qcamera-preview-dump-once '+REMOTE+'/qcamera-probe.log '+REMOTE+'/rootfs/work/camera-frame.yuv '+REMOTE+'/rootfs/work/camera-photo.jpg'],check=True,capture_output=True)
subprocess.run(adb+['push',str(args.camera_preview_binary),REMOTE+'/qcamera-preview-dump'],check=True,capture_output=True)
subprocess.run(adb+['shell','chmod 755 '+REMOTE+'/qcamera-preview-dump; touch '+REMOTE+'/qcamera-preview-dump-once'],check=True,capture_output=True)
+ if args.camera_service_probe:
+ if not args.camera_service_probe_binary or not args.camera_service_probe_binary.is_file():
+ raise RuntimeError('--camera-service-probe requires --camera-service-probe-binary')
+ if not args.camera_permission_bridge_binary or not args.camera_permission_bridge_binary.is_file():
+ raise RuntimeError('--camera-service-probe requires --camera-permission-bridge-binary')
+ if not args.camera_permission_bridge_runtime or not args.camera_permission_bridge_runtime.is_file():
+ raise RuntimeError('--camera-service-probe requires --camera-permission-bridge-runtime')
+ probe='/data/local/tmp/vulpes-camera-probe-20261023'
+ subprocess.run(adb+['push',str(args.camera_service_probe_binary),probe],check=True,capture_output=True)
+ bridge='/data/local/tmp/vulpes-camera-permission-bridge'
+ runtime='/data/local/tmp/libc++_shared.so'
+ subprocess.run(adb+['push',str(args.camera_permission_bridge_binary),bridge],check=True,capture_output=True)
+ subprocess.run(adb+['push',str(args.camera_permission_bridge_runtime),runtime],check=True,capture_output=True)
+ subprocess.run(adb+['shell','chmod 755 '+probe+' '+bridge+' '+runtime+'; rm -f '+REMOTE+'/camera-permission-bridge.log; touch '+REMOTE+'/camera-service-probe-once'],check=True,capture_output=True)
+ if args.camera_gl_probe:
+ if not args.camera_gl_probe_binary or not args.camera_gl_probe_binary.is_file():
+ raise RuntimeError('--camera-gl-probe requires --camera-gl-probe-binary')
+ if not args.camera_permission_bridge_binary or not args.camera_permission_bridge_binary.is_file():
+ raise RuntimeError('--camera-gl-probe requires --camera-permission-bridge-binary')
+ if not args.camera_permission_bridge_runtime or not args.camera_permission_bridge_runtime.is_file():
+ raise RuntimeError('--camera-gl-probe requires --camera-permission-bridge-runtime')
+ probe='/data/local/tmp/vulpes-camera-gl-probe-20261023'
+ subprocess.run(adb+['push',str(args.camera_gl_probe_binary),probe],check=True,capture_output=True)
+ bridge='/data/local/tmp/vulpes-camera-permission-bridge'
+ runtime='/data/local/tmp/libc++_shared.so'
+ subprocess.run(adb+['push',str(args.camera_permission_bridge_binary),bridge],check=True,capture_output=True)
+ subprocess.run(adb+['push',str(args.camera_permission_bridge_runtime),runtime],check=True,capture_output=True)
+ subprocess.run(adb+['shell','chmod 755 '+probe+' '+bridge+' '+runtime+'; rm -f '+REMOTE+'/camera-permission-bridge.log; touch '+REMOTE+'/camera-gl-probe-once'],check=True,capture_output=True)
+ if camera_broker:
+ subprocess.run([sys.executable,str(ROOT/'tundra/tools/build-beatles-camera.py')],check=True)
+ broker_build=OUT/'camera-broker-build'
+ worker_path=args.camera_broker_worker or broker_build/'beatles-camera-worker'
+ broker_path=args.camera_broker_binary or broker_build/'beatles-camera-broker'
+ permission_bridge_path=args.camera_permission_bridge_binary or OUT/'camera-probe-runtime/beatles-camera-permission-bridge'
+ permission_runtime_path=args.camera_permission_bridge_runtime or OUT/'camera-probe-runtime/libc++_shared.so'
+ if not worker_path.is_file():
+ raise RuntimeError('CameraService worker binary not found: '+str(worker_path))
+ if not broker_path.is_file():
+ raise RuntimeError('CameraService broker binary not found: '+str(broker_path))
+ if not permission_bridge_path.is_file():
+ raise RuntimeError('Camera permission bridge binary not found: '+str(permission_bridge_path))
+ if not permission_runtime_path.is_file():
+ raise RuntimeError('Camera permission bridge runtime not found: '+str(permission_runtime_path))
+ broker_worker='/data/local/tmp/vulpes-beatles-camera-worker'
+ broker='/data/local/tmp/vulpes-beatles-camera-broker'
+ bridge='/data/local/tmp/vulpes-camera-permission-bridge'
+ runtime='/data/local/tmp/libc++_shared.so'
+ for source,target in ((worker_path,broker_worker),(broker_path,broker),
+ (permission_bridge_path,bridge),(permission_runtime_path,runtime)):
+ subprocess.run(adb+['push',str(source),target],check=True,capture_output=True)
+ subprocess.run(adb+['shell','chmod 755 '+broker_worker+' '+broker+' '+bridge+' '+runtime+'; mkdir -p '+REMOTE+'/camera-ipc; rm -f '+REMOTE+'/camera-ipc/camera.sock '+REMOTE+'/camera-permission-bridge.log '+REMOTE+'/camera-broker.log; touch '+REMOTE+'/camera-broker-once'],check=True,capture_output=True)
subprocess.run(adb+['push',str(ROOT/'tundra/devices/beatles/t9'),REMOTE+'/rootfs/opt/vulpes/tundra/devices/beatles/'],check=True,capture_output=True,timeout=120)
# Seed the isolated Gaia media store with small, known-good playback samples.
media=REMOTE+'/rootfs/work/gaia-profile/vulpes-media'
@@ -86,7 +200,9 @@ def main():
for directory in ('host','services','adapters'):
subprocess.run(adb+['push',str(ROOT/directory),REMOTE+'/rootfs/opt/firefox/vulpes/'],check=True,capture_output=True,timeout=120)
subprocess.run(adb+['push',str(ROOT/'host'),REMOTE+'/rootfs/opt/vulpes/'],check=True,capture_output=True,timeout=120)
- files=((OUT/'control-client','rootfs/work/control-client'),(OUT/'display-session','display-session'),(OUT/'keypad','rootfs/work/keypad'),
+ subprocess.run(adb+['shell','mkdir -p '+REMOTE+'/rootfs/opt/vulpes/tundra/runtime'],check=True,capture_output=True)
+ subprocess.run(adb+['push',str(ROOT/'tundra/runtime/beatles-camera-client.py'),REMOTE+'/rootfs/opt/vulpes/tundra/runtime/beatles-camera-client.py'],check=True,capture_output=True)
+ files=((OUT/'control-client','rootfs/work/control-client'),(display_session,'display-session'),(OUT/'keypad','rootfs/work/keypad'),
(OUT/'gaia-test.sh','rootfs/work/display-test.sh'),
(ROOT/'tundra/devices/beatles/gaia-session.py','rootfs/work/gaia-session.py'),
(ROOT/'tundra/devices/beatles/cover-display.py','rootfs/work/cover-display.py'),
@@ -113,6 +229,9 @@ def main():
# Its restoration marker must not be mistaken for this trial.
subprocess.run(adb+['shell','rm -f '+REMOTE+'/display-session.done'],check=True,capture_output=True)
script='''(function(){var Cc=Components.classes,Ci=Components.interfaces;
+ var b=Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+ b.setBoolPref("devtools.debugger.remote-enabled",true);
+ Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService).savePrefFile(null);
var f=Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);f.initWithPath(%s);
var p=Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);p.init(f);
p.runAsync([%s],1,null,false);return "started";})()''' % (json.dumps(REMOTE+'/display-session'),json.dumps(session_option))
@@ -141,7 +260,7 @@ def main():
time.sleep(1)
subprocess.run(adb+['shell','touch '+REMOTE+'/stop-session'],check=True,capture_output=True)
raise RuntimeError('Manual session startup timed out; requested KaiOS restoration')
- for second in range(205):
+ for second in range(265):
state=subprocess.check_output(adb+['shell','cat '+REMOTE+'/display-session.done'],text=True).strip()
if state=='restored':break
if second and second%30==0:print('Trial running:',second,'seconds',flush=True)
@@ -149,12 +268,19 @@ def main():
else:raise RuntimeError('No restoration marker; inspect the watchdog before retrying')
report=OUT/('gaia-trial-'+time.strftime('%Y%m%d-%H%M%S'))
report.mkdir()
- for relative in ('display-session.log','qcamera-probe.log','framebuffer.ppm','rootfs/work/camera-frame.yuv','rootfs/work/camera-photo.jpg','rootfs/work/Xorg.log','rootfs/work/xorg-console.log',
+ for relative in ('display-session.log','qcamera-probe.log','camera-service-probe.log','camera-gl-probe.log','camera-permission-bridge.log','camera-broker.log','framebuffer.ppm','rootfs/work/camera-frame.yuv','rootfs/work/camera-photo.jpg','rootfs/work/Xorg.log','rootfs/work/xorg-console.log',
'rootfs/run/tundra-hardware/status.json','rootfs/work/radio-status.log',
'rootfs/work/cover-display.log','rootfs/work/cover-preview.png','rootfs/work/cover-refresh.json','rootfs/work/lid-state.json',
'rootfs/work/keypad.log','rootfs/work/gecko.log','rootfs/work/gaia-server.log',
'rootfs/work/gaia-audit.json','rootfs/work/gaia-native.png','rootfs/work/memory-samples.jsonl'):
subprocess.run(adb+['pull',REMOTE+'/'+relative,str(report/Path(relative).name)],capture_output=True)
+ if args.camera_gl_probe:
+ for remote, name in (('/data/local/tmp/vulpes-camera-glconsumer.ppm','camera-glconsumer.ppm'),
+ ('/data/local/tmp/vulpes-camera-gonkqueue.nv21','camera-gonkqueue.nv21'),
+ ('/data/local/tmp/vulpes-camera-gonkqueue.txt','camera-gonkqueue.txt'),
+ ('/data/local/tmp/vulpes-camera-callback.yuv','camera-callback.yuv'),
+ ('/data/local/tmp/vulpes-camera-callback.jpg','camera-callback.jpg')):
+ subprocess.run(adb+['pull',remote,str(report/name)],capture_output=True)
service=subprocess.check_output(adb+['shell','getprop init.svc.b2g'],text=True).strip()
print('KaiOS service:',service,'Report:',report,flush=True)
# Keep both outputs from the bounded Qualcomm probe. Its still-snapshot
@@ -170,6 +296,10 @@ def main():
if service!='running':raise RuntimeError('KaiOS did not restart')
if args.camera_preview_binary:
subprocess.run(adb+['shell','rm -f '+REMOTE+'/qcamera-preview-dump '+REMOTE+'/qcamera-preview-dump-once'],capture_output=True)
+ if args.camera_service_probe:
+ subprocess.run(adb+['shell','rm -f /data/local/tmp/vulpes-camera-probe-20261023 /data/local/tmp/vulpes-camera-permission-bridge /data/local/tmp/libc++_shared.so /data/local/tmp/vulpes-camera-frame.nv21'],capture_output=True)
+ if args.camera_gl_probe:
+ subprocess.run(adb+['shell','rm -f /data/local/tmp/vulpes-camera-gl-probe-20261023 /data/local/tmp/vulpes-camera-permission-bridge /data/local/tmp/libc++_shared.so'],capture_output=True)
if not (report/'gaia-audit.json').is_file():raise RuntimeError('Gaia audit did not complete')
audit=json.loads((report/'gaia-audit.json').read_text())
summary={key:value for key,value in audit.items() if key not in ('icons','layout')}
diff --git a/tundra/tools/shared_ui.py b/tundra/tools/shared_ui.py
index a7a7659a..de1be627 100644
--- a/tundra/tools/shared_ui.py
+++ b/tundra/tools/shared_ui.py
@@ -51,6 +51,8 @@ def selected_files():
*sorted((ROOT.parent/'services').rglob('*.mjs')),
*sorted((ROOT/'mms-codec').glob('*')),
*sorted((ROOT.parent/'assets/webapps/marketplace.gaiamobile.org').rglob('*')),
+ ROOT.parent/'assets/webapps/system.gaiamobile.org/style/system/system.css',
+ ROOT.parent/'assets/webapps/system.gaiamobile.org/style/system/initlogo.css',
ROOT.parent/'tools/installed_apps.py',
*sorted((ROOT.parent/'gaia/shared/elements/vulpes-fonts').rglob('*')),
*sorted((ROOT.parent/'overrides').rglob('*')),
@@ -64,6 +66,8 @@ def selected_files():
ROOT.parent/'assets/webapps/settings.gaiamobile.org/js/panels/app_permissions_list/panel.js',
ROOT.parent/'assets/webapps/settings.gaiamobile.org/js/panels/screen_lock_passcode/panel.js',
ROOT.parent/'assets/webapps/verticalhome.gaiamobile.org/gaia_build_defer_index.js',
+ ROOT.parent/'assets/webapps/verticalhome.gaiamobile.org/js/init.json',
+ ROOT.parent/'gaia/apps/verticalhome/js/sources/application.js',
ROOT.parent/'assets/webapps/wallpaper.gaiamobile.org/style/wallpaper.css',
ROOT.parent/'assets/webapps/ringtones.gaiamobile.org/gaia_build_defer_pick.js',
ROOT.parent/'assets/webapps/ringtones.gaiamobile.org/gaia_build_defer_manage.js',
diff --git a/tundra/tools/storage_layout.py b/tundra/tools/storage_layout.py
index 2827bd14..765ef6cb 100644
--- a/tundra/tools/storage_layout.py
+++ b/tundra/tools/storage_layout.py
@@ -9,6 +9,18 @@ def mount_table(layout):
raise ValueError('Expected Sargo userdata, not another partition')
if not re.fullmatch(r'[0-9a-f]{64}', layout.get('firstMiBSha256', '')):
raise ValueError('Missing userdata identity checksum')
+ if layout.get('schema') == 2 and layout.get('table') == 'direct':
+ if (layout.get('partitionLabel') != 'userdata' or
+ layout.get('filesystem') != 'ext4' or
+ layout.get('storage') != 'raw-userdata-ext4' or
+ not re.fullmatch(r'[0-9]+:[0-9]+', layout.get('backingDevice', '')) or
+ not re.fullmatch(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
+ layout.get('filesystemUuid', ''))):
+ raise ValueError('Direct mapping requires inventoried raw ext4 userdata')
+ size = int(layout.get('size', 0))
+ if size <= 0 or size % 512:
+ raise ValueError('Invalid direct userdata geometry')
+ return None
match = re.fullmatch(r'0 ([0-9]+) linear ([0-9]+:[0-9]+) ([0-9]+)',
layout.get('table', ''))
if not match:
diff --git a/tundra/tools/sync-beatles-gaia.py b/tundra/tools/sync-beatles-gaia.py
index 8506abf6..c222e19a 100644
--- a/tundra/tools/sync-beatles-gaia.py
+++ b/tundra/tools/sync-beatles-gaia.py
@@ -36,11 +36,34 @@ def main():
spec=importlib.util.spec_from_file_location('package_gaia',ROOT/'tundra/tools/package-beatles-gaia.py')
package=importlib.util.module_from_spec(spec);spec.loader.exec_module(package)
files=package.shared_files()
- current={str(p.relative_to(ROOT)):hashlib.sha256(p.read_bytes()).hexdigest() for p in files}
+ out=ROOT/'tundra/out/beatles/gecko-156.0.1'
+ cache_path=out/'.gaia-sync-hash-cache.json'
+ try:
+ cache=json.loads(cache_path.read_text()).get('files',{})
+ except (OSError,ValueError,AttributeError):
+ cache={}
+ current={}
+ next_cache={}
+ reused=0
+ for path in files:
+ name=str(path.relative_to(ROOT))
+ stat=path.stat()
+ fingerprint={'device':stat.st_dev,'inode':stat.st_ino,'size':stat.st_size,
+ 'mtime_ns':stat.st_mtime_ns,'ctime_ns':stat.st_ctime_ns}
+ previous=cache.get(name,{})
+ digest=previous.get('sha256') if all(previous.get(k)==v for k,v in fingerprint.items()) else None
+ if not digest:
+ digest=hashlib.sha256(path.read_bytes()).hexdigest()
+ else:
+ reused+=1
+ current[name]=digest
+ next_cache[name]={**fingerprint,'sha256':digest}
+ temporary=cache_path.with_suffix('.tmp')
+ temporary.write_text(json.dumps({'files':next_cache},separators=(',',':'))+'\n')
+ temporary.replace(cache_path)
raw=shell('cat '+REMOTE+'/work/gaia-sync.json 2>/dev/null || true')
previous=json.loads(raw).get('files',{}) if raw else {}
changed,removed=changes(current,previous)
- out=ROOT/'tundra/out/beatles/gecko-156.0.1'
manifest=out/'gaia-sync.json'
manifest.write_text(json.dumps({'files':current},indent=2)+'\n')
if changed or removed:
@@ -57,6 +80,6 @@ def main():
shell('rm -f '+' '.join(shlex.quote(REMOTE+'/opt/vulpes/'+p) for p in removed[start:start+100]))
subprocess.run(adb+['push',str(manifest),REMOTE+'/work/gaia-sync.json.new'],check=True,capture_output=True)
shell('mv '+REMOTE+'/work/gaia-sync.json.new '+REMOTE+'/work/gaia-sync.json')
- print(json.dumps({'files':len(current),'updated':len(changed),'removed':len(removed),'profilePreserved':True}))
+ print(json.dumps({'files':len(current),'hashesReused':reused,'updated':len(changed),'removed':len(removed),'profilePreserved':True}))
if __name__=='__main__':main()