API du pont JavaScript
Contacts
Lire, rechercher, choisir et écrire - le sélecteur ne demande aucune autorisation.
addContact pont#
window.WebToApk.addContact(requestId: String, contactJson: String)
Le sélecteur système (aucune autorisation requise) et l'écriture d'un nouveau contact (WRITE_CONTACTS). La partie lecture - lister et rechercher - se trouve dans le groupe Contacts ci-dessus.
Décrit par son groupe, Contacts : choisir et ajouter, plutôt que séparément.
Exemple
Enregistre un nouveau contact directement dans le carnet d'adresses du téléphone, sans ouvrir l'application Contacts.
Retour : rien pour l'instant. La réponse est l'événement appmint:contacts (et window.onAppMintContacts) avec detail = { requestId, ok: true, id: '123' } en cas de succès, { requestId, ok: false, id: null } si l'enregistrement a échoué, ou { requestId, error }. Nécessite : activez Modifier les contacts à l'étape 4 (Accès) lors de la compilation ; l'autorisation est demandée à l'utilisateur au premier appel.
Enregistrer un client. Le contact est transmis sous forme de chaîne JSON.
function saveCustomer(name, phone, email) {
if (!(window.WebToApk && window.WebToApk.addContact)) return;
var id = 'add-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error === 'not_enabled') { showMessage('Turn on Edit Contacts when you build'); return; }
if (e.detail.error) { showMessage('Not saved: ' + e.detail.error); return; }
showMessage(e.detail.ok ? 'Saved to contacts' : 'Could not save the contact');
});
WebToApk.addContact(id, JSON.stringify({
displayName: name,
phones: [{ number: phone, type: 'mobile' }], // type: mobile | home | work
emails: [email]
}));
}Forme courte. Les valeurs uniques sont acceptées aussi : { name: 'Ali', phone: '+911234567890', email: '[email protected]' }.
Un bouton « Enregistrer dans les contacts » sur une page de carte de visite, avec la forme courte et une Promise :
function addOne(contact) {
return new Promise(function (resolve, reject) {
var id = 'card-' + Date.now() + '-' + Math.random().toString(36).slice(2);
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error) reject(new Error(e.detail.error));
else if (!e.detail.ok) reject(new Error('save_failed'));
else resolve(e.detail.id);
});
WebToApk.addContact(id, JSON.stringify(contact));
});
}
document.getElementById('save-card').addEventListener('click', function () {
if (!(window.WebToApk && window.WebToApk.addContact)) return;
addOne({ name: 'Priya Sharma', phone: '+919876543210', email: '[email protected]' })
.then(function () { showMessage('Priya is in your contacts now'); })
.catch(function (err) {
showMessage(err.message === 'permission_denied' ? 'Allow Contacts to save this card.' : 'Not saved: ' + err.message);
});
});Importer une liste d'équipe personne par personne, avec numéros pro et perso, et indiquer combien ont été enregistrés :
async function importTeam(team) { // [{ name, work, home, email }]
if (!(window.WebToApk && window.WebToApk.addContact)) return;
var saved = 0;
for (var i = 0; i < team.length; i++) {
var m = team[i];
try {
await addOne({ // addOne() from the example above
displayName: m.name,
phones: [{ number: m.work, type: 'work' }, { number: m.home, type: 'home' }],
emails: m.email ? [m.email] : []
});
saved++;
} catch (err) {
if (err.message === 'not_enabled' || err.message === 'permission_denied') break; // the rest would fail too
}
}
showMessage(saved + ' of ' + team.length + ' saved');
}Remarques : Erreurs : not_enabled, permission_denied. Le id renvoyé est le nouvel identifiant de contact brut. Un type de téléphone autre que home ou work est enregistré comme mobile. Le contact est enregistré sur l'appareil (sans compte). L'écriture des contacts est une autorisation sensible : votre fiche Play Store doit la déclarer dans le formulaire Sécurité des données et dans la politique de confidentialité.
getContact pont#
window.WebToApk.getContact(requestId: String, contactId: String)
Tout est asynchrone : la lecture du curseur peut porter sur des milliers de lignes, et un retour bloquant de @JavascriptInterface l'exécuterait sur le thread JS - un ANR. Les résultats arrivent sous la forme `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId : passez une CHAÎNE ('pick-' + Date.now()). Un NOMBRE brut fonctionne aussi - le pont le convertit via un double ("1.787491234567E12"), et chaque réponse le remet sous sa forme canonique (BridgeRequestId), donc `res.requestId === Date.now()` est vrai en comparaison stricte dans les deux cas. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- AUCUNE autorisation requise (sélecteur système) WebToApk.addContact(id, json) <- nécessite l'autorisation d'écriture des contacts
Décrit par son groupe, Contacts, plutôt que séparément.
Exemple
Relit un contact par son id (le id provenant de listContacts ou searchContacts).
Retour : rien pour l'instant. La réponse est l'événement appmint:contacts (et window.onAppMintContacts) avec detail = { requestId, contact: { id, displayName, photoUri, phones: [{ number, type }], emails: [...] } }. contact vaut null quand ce contact n'existe plus. En cas d'échec : { requestId, error }. Nécessite : activez Contacts à l'étape 4 (Accès) lors de la compilation.
Actualiser un contact enregistré avec le hook de la page plutôt qu'un écouteur d'événement.
var pendingId = '';
window.onAppMintContacts = function (detail) {
if (detail.requestId !== pendingId) return;
if (detail.error) { showMessage('Could not read the contact: ' + detail.error); return; }
if (!detail.contact) { showMessage('This contact was deleted.'); return; }
var c = detail.contact;
document.getElementById('name').textContent = c.displayName;
document.getElementById('phones').textContent =
c.phones.map(function (p) { return p.type + ': ' + p.number; }).join(', ');
document.getElementById('emails').textContent = c.emails.join(', ');
};
function openContact(contactId) {
if (!(window.WebToApk && window.WebToApk.getContact)) return;
pendingId = 'one-' + Date.now();
WebToApk.getContact(pendingId, String(contactId));
}Actualiser une liste enregistrée d'identifiants de contacts favoris et retirer ceux supprimés du téléphone depuis :
function getOne(contactId) {
return new Promise(function (resolve, reject) {
var id = 'fav-' + contactId + '-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error) reject(new Error(e.detail.error)); else resolve(e.detail.contact);
});
WebToApk.getContact(id, String(contactId));
});
}
async function loadFavourites() {
if (!(window.WebToApk && window.WebToApk.getContact)) return;
var ids = JSON.parse(localStorage.getItem('favIds') || '[]');
var people = [];
for (var i = 0; i < ids.length; i++) {
var c = await getOne(ids[i]);
if (c) people.push(c); // null = deleted from the phone
}
localStorage.setItem('favIds', JSON.stringify(people.map(function (c) { return c.id; })));
renderFavourites(people);
}Vérifier si le numéro que votre application a enregistré pour un client figure toujours sur sa fiche de contact :
function checkStoredNumber(customer) { // { contactId, phone }
if (!(window.WebToApk && window.WebToApk.getContact)) return;
var id = 'check-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error) { showMessage('Could not check: ' + e.detail.error); return; }
if (!e.detail.contact) { showMessage('This customer is no longer in your contacts.'); return; }
var digits = function (s) { return String(s).replace(/\D/g, ''); };
var still = e.detail.contact.phones.some(function (p) {
return digits(p.number).slice(-10) === digits(customer.phone).slice(-10);
});
if (!still) showMessage('The number for ' + e.detail.contact.displayName + ' has changed.');
});
WebToApk.getContact(id, String(customer.contactId));
}Remarques : window.onAppMintContacts reçoit toutes les réponses des contacts (liste, recherche, sélection, ajout) ; vérifiez donc toujours requestId. Vous pouvez l'utiliser ou addEventListener('appmint:contacts', ...) - les deux se déclenchent. Erreurs : not_enabled, permission_denied, foreign_origin. Le id renvoyé par pickContact est un identifiant de ligne de téléphone, pas un identifiant de contact, il ne fonctionne donc pas ici. Les contacts sont des données personnelles : votre fiche Play Store doit les déclarer dans le formulaire Sécurité des données et dans la politique de confidentialité.
listContacts pont#
window.WebToApk.listContacts(requestId: String, limit: Int, offset: Int)
Tout est asynchrone : la lecture du curseur peut porter sur des milliers de lignes, et un retour bloquant de @JavascriptInterface l'exécuterait sur le thread JS - un ANR. Les résultats arrivent sous la forme `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId : passez une CHAÎNE ('pick-' + Date.now()). Un NOMBRE brut fonctionne aussi - le pont le convertit via un double ("1.787491234567E12"), et chaque réponse le remet sous sa forme canonique (BridgeRequestId), donc `res.requestId === Date.now()` est vrai en comparaison stricte dans les deux cas. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- AUCUNE autorisation requise (sélecteur système) WebToApk.addContact(id, json) <- nécessite l'autorisation d'écriture des contacts
Décrit par son groupe, Contacts, plutôt que séparément.
Exemple
Lit le carnet d'adresses du téléphone, page par page, trié de A à Z par nom.
Retour : rien pour l'instant. La réponse arrive plus tard sous la forme de l'événement appmint:contacts (et window.onAppMintContacts) avec detail = { requestId, contacts: [{ id, displayName, photoUri, phones: [{ number, type }], emails: ['[email protected]'] }] }, ou { requestId, error }. Nécessite : activez Contacts à l'étape 4 (Accès) lors de la compilation ; l'autorisation est demandée à l'utilisateur au premier appel.
Charger les 100 premiers contacts. Associez la réponse sur votre propre requestId.
function loadContacts() {
if (!(window.WebToApk && window.WebToApk.listContacts)) {
showMessage('Contacts work only inside the app.');
return;
}
var id = 'contacts-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return; // another call's answer
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error) { showMessage('Could not read contacts: ' + e.detail.error); return; }
var list = document.getElementById('list');
list.innerHTML = '';
e.detail.contacts.forEach(function (c) {
var li = document.createElement('li');
var num = c.phones.length ? c.phones[0].number + ' (' + c.phones[0].type + ')' : '';
li.textContent = c.displayName + ' ' + num;
list.appendChild(li);
});
});
WebToApk.listContacts(id, 100, 0); // limit 100, start at 0
}Page suivante. Passez le nombre que vous avez déjà comme offset: WebToApk.listContacts(id, 100, 100).
Charger tout le carnet d'adresses par pages de 500 jusqu'à ce qu'une page incomplète indique la fin :
function listPage(limit, offset) {
return new Promise(function (resolve, reject) {
var id = 'page-' + offset + '-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error) reject(new Error(e.detail.error));
else resolve(e.detail.contacts);
});
WebToApk.listContacts(id, limit, offset);
});
}
async function loadEveryContact() {
if (!(window.WebToApk && window.WebToApk.listContacts)) return [];
var all = [];
while (true) {
var page = await listPage(500, all.length);
all = all.concat(page);
if (page.length < 500) return all; // last page
}
}Construire une liste « Inviter des amis » à partir des seuls numéros mobiles, et indiquer à l'utilisateur quoi faire quand la lecture est refusée :
function showInviteList() {
if (!(window.WebToApk && window.WebToApk.listContacts)) return;
var id = 'invite-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error === 'permission_denied') { showMessage('Allow Contacts to invite friends.'); return; }
if (e.detail.error) { showMessage('Contacts are not available: ' + e.detail.error); return; }
var mobiles = [];
e.detail.contacts.forEach(function (c) {
c.phones.forEach(function (p) {
if (p.type === 'mobile') mobiles.push({ name: c.displayName, number: p.number });
});
});
renderInviteList(mobiles); // your own list UI
});
WebToApk.listContacts(id, 500, 0);
}Remarques : limit est limité à 1-2000. Erreurs : not_enabled (Contacts était désactivé à la compilation), permission_denied (l'utilisateur a refusé), foreign_origin (appel depuis une page qui n'appartient pas à votre application). Le type du téléphone vaut home, mobile, work, main, work_fax, home_fax ou other. photoUri est une adresse content://, pas une URL d'image qu'une page peut charger. Pour laisser l'utilisateur choisir une seule personne, utilisez plutôt pickContact : aucune autorisation n'est nécessaire. Les contacts sont des données personnelles : votre fiche Play Store doit les déclarer dans le formulaire Sécurité des données et dans la politique de confidentialité.
pickContact pont#
window.WebToApk.pickContact(requestId: String)
Sélecteur de contacts du système - ne demande AUCUNE autorisation, et c'est le bon choix chaque fois que la page veut simplement que l'utilisateur choisisse une personne. Il choisit dans la table PHONE, pas dans la table des contacts : le sélecteur n'accorde l'accès en lecture qu'à la ligne qu'il retourne, et c'est donc le choix d'une ligne de téléphone qui rend le numéro lisible sans READ_CONTACTS. Choisir un contact seul retournerait une ligne avec un nom et sans numéro, et lire le numéro à partir de celle-ci demanderait l'autorisation que cette méthode sert justement à éviter.
Exemple
Ouvre le sélecteur de contacts d'Android pour que l'utilisateur choisisse un numéro de téléphone. Aucune autorisation n'est nécessaire.
Retour : rien pour l'instant. La réponse est l'événement appmint:contacts (et window.onAppMintContacts) avec detail = { requestId, contact: { id, displayName, photoUri, phones: [{ number, type: 'picked' }], emails: [] } }. Si l'utilisateur ferme le sélecteur : { requestId, error: 'cancelled' }. Nécessite : rien - aucune option de compilation, aucune demande d'autorisation.
Remplir un formulaire avec la personne choisie.
function chooseContact() {
if (!(window.WebToApk && window.WebToApk.pickContact)) {
showMessage('Open this page in the app to pick a contact.');
return;
}
var id = 'pick-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error === 'cancelled') return; // user closed the picker
if (e.detail.error) { showMessage('Could not open contacts'); return; }
if (!e.detail.contact) { showMessage('Could not read that contact'); return; }
var c = e.detail.contact;
document.getElementById('name').value = c.displayName;
document.getElementById('phone').value = c.phones.length ? c.phones[0].number : '';
});
WebToApk.pickContact(id);
}Garder une courte liste de contacts d'urgence. Enregistrez le nom et le numéro, pas le id, afin que la liste ne dépende pas du carnet d'adresses :
function pickOne() {
return new Promise(function (resolve, reject) {
var id = 'sos-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error === 'cancelled') resolve(null);
else if (e.detail.error) reject(new Error(e.detail.error));
else resolve(e.detail.contact);
});
WebToApk.pickContact(id);
});
}
document.getElementById('add-sos').addEventListener('click', async function () {
if (!(window.WebToApk && window.WebToApk.pickContact)) return;
var c = await pickOne();
if (!c || !c.phones.length) return; // closed, or nothing readable
var list = JSON.parse(localStorage.getItem('sos') || '[]');
list.push({ name: c.displayName, number: c.phones[0].number });
localStorage.setItem('sos', JSON.stringify(list));
renderSosList(list);
});Désactiver le bouton pendant que le sélecteur est ouvert, et afficher les champs de saisie quand le téléphone n'a pas d'application de contacts :
var pickBtn = document.getElementById('pick');
pickBtn.addEventListener('click', function () {
if (!(window.WebToApk && window.WebToApk.pickContact)) { showManualEntry(); return; }
var id = 'recipient-' + Date.now();
pickBtn.disabled = true; // one picker at a time
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
pickBtn.disabled = false;
if (e.detail.error === 'no_picker') { showManualEntry(); return; }
if (e.detail.error) return; // 'cancelled'
if (e.detail.contact && e.detail.contact.phones.length) {
document.getElementById('to').value = e.detail.contact.phones[0].number;
}
});
WebToApk.pickContact(id);
});Remarques : Vous obtenez le nom et le SEUL numéro touché par l'utilisateur - jamais d'e-mails, jamais le reste du carnet. emails est toujours vide. Erreurs : cancelled, no_picker (aucune application de contacts sur le téléphone). Si la lecture échoue après une sélection, contact vaut null. Comme rien n'est lu sans le choix de l'utilisateur, c'est la méthode adaptée à Google Play pour obtenir un numéro de téléphone ; préférez-la à listContacts quand vous n'avez besoin que d'une personne. Pour les applications créées par IA et les imports Google AI Studio, la compilation analyse votre code ; un appel à pickContact( ne coche pas Contacts - seuls listContacts, searchContacts et getContact le font - donc le sélecteur n'ajoute aucune autorisation de lecture des contacts.
searchContacts pont#
window.WebToApk.searchContacts(requestId: String, query: String, limit: Int)
Tout est asynchrone : la lecture du curseur peut porter sur des milliers de lignes, et un retour bloquant de @JavascriptInterface l'exécuterait sur le thread JS - un ANR. Les résultats arrivent sous la forme `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId : passez une CHAÎNE ('pick-' + Date.now()). Un NOMBRE brut fonctionne aussi - le pont le convertit via un double ("1.787491234567E12"), et chaque réponse le remet sous sa forme canonique (BridgeRequestId), donc `res.requestId === Date.now()` est vrai en comparaison stricte dans les deux cas. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- AUCUNE autorisation requise (sélecteur système) WebToApk.addContact(id, json) <- nécessite l'autorisation d'écriture des contacts
Décrit par son groupe, Contacts, plutôt que séparément.
Exemple
Trouve les contacts dont le nom correspond à un mot de recherche, sans charger tout le carnet d'adresses.
Retour : rien pour l'instant. La réponse est l'événement appmint:contacts (et window.onAppMintContacts) avec detail = { requestId, contacts: [...] } - le même format de contact que listContacts - ou { requestId, error }. Nécessite : activez Contacts à l'étape 4 (Accès) lors de la compilation ; l'autorisation est demandée à l'utilisateur au premier appel.
Rechercher pendant la saisie. Seule la réponse à la dernière recherche est affichée.
var lastSearchId = '';
function searchPeople(word) {
if (!(window.WebToApk && window.WebToApk.searchContacts)) return;
lastSearchId = 'search-' + Date.now();
WebToApk.searchContacts(lastSearchId, word, 20); // at most 20 results
}
window.addEventListener('appmint:contacts', function (e) {
if (!e.detail || e.detail.requestId !== lastSearchId) return; // old or other request
if (e.detail.error) { showMessage('Search failed: ' + e.detail.error); return; }
var names = e.detail.contacts.map(function (c) {
return c.displayName + (c.phones.length ? ' - ' + c.phones[0].number : '');
});
document.getElementById('results').textContent = names.join('\n') || 'No match';
});
document.getElementById('q').addEventListener('input', function (e) {
searchPeople(e.target.value);
});Envelopper la recherche dans une Promise et attendre que l'utilisateur arrête de taper, pour qu'une saisie rapide ne lance pas une recherche par touche :
function searchOnce(word, limit) {
return new Promise(function (resolve, reject) {
var id = 'find-' + Date.now() + '-' + Math.random().toString(36).slice(2);
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error) reject(new Error(e.detail.error)); else resolve(e.detail.contacts);
});
WebToApk.searchContacts(id, word, limit);
});
}
var timer = null;
document.getElementById('q').addEventListener('input', function (e) {
clearTimeout(timer);
var word = e.target.value.trim();
timer = setTimeout(function () {
if (!(window.WebToApk && window.WebToApk.searchContacts) || !word) return;
searchOnce(word, 10)
.then(function (people) { renderSuggestions(people); })
.catch(function (err) { showMessage('Search failed: ' + err.message); });
}, 300);
});Remplir le champ e-mail depuis le carnet d'adresses quand le nom saisi correspond à une seule personne ayant un e-mail :
function suggestEmail(name) {
if (!(window.WebToApk && window.WebToApk.searchContacts) || name.length < 2) return;
var id = 'email-' + Date.now();
window.addEventListener('appmint:contacts', function handler(e) {
if (!e.detail || e.detail.requestId !== id) return;
window.removeEventListener('appmint:contacts', handler);
if (e.detail.error) return; // leave the box for the user to type
var withEmail = e.detail.contacts.filter(function (c) { return c.emails.length > 0; });
if (withEmail.length === 1) {
document.getElementById('email').value = withEmail[0].emails[0];
}
});
WebToApk.searchContacts(id, name, 5);
}
document.getElementById('name').addEventListener('change', function (e) {
suggestEmail(e.target.value.trim());
});Remarques : Un mot de recherche vide renvoie les contacts du début de la liste (comme listContacts). limit est limité à 1-2000 et il n'y a pas de décalage. Erreurs : not_enabled, permission_denied, foreign_origin. Les contacts sont des données personnelles : votre fiche Play Store doit les déclarer dans le formulaire Sécurité des données et dans la politique de confidentialité.