API del puente de JavaScript
Contactos
Leer, buscar, elegir y escribir: el selector no necesita ningún permiso.
addContact puente#
window.WebToApk.addContact(requestId: String, contactJson: String)
El selector del sistema (no necesita permiso) y la escritura de un contacto nuevo (WRITE_CONTACTS). La parte de lectura (listar y buscar) está en el grupo Contactos de arriba.
Se describe en su grupo, Contactos: elegir y añadir, y no por separado.
Ejemplo
Guarda un contacto nuevo directamente en la agenda del teléfono, sin abrir la app de Contactos.
Devuelve: nada por ahora. La respuesta es el evento appmint:contacts (y window.onAppMintContacts) con detail = { requestId, ok: true, id: '123' } si tiene éxito, { requestId, ok: false, id: null } cuando falló el guardado, o { requestId, error }. Requiere: activar Editar contactos en el Paso 4 (Acceso) al compilar; al usuario se le pide permiso en la primera llamada.
Guarda un cliente. El contacto se pasa como una cadena 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]
}));
}Forma corta. También se aceptan valores sueltos: { name: 'Ali', phone: '+911234567890', email: '[email protected]' }.
Un botón "Guardar en contactos" en la página de una tarjeta de visita, usando la forma corta y una 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);
});
});Importa la lista de un equipo persona por persona, con números de trabajo y de casa, e informa de cuántos se guardaron:
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');
}Notas: Errores: not_enabled, permission_denied. El id devuelto es el nuevo id de contacto sin procesar. Un type de teléfono distinto de home o work se guarda como mobile. El contacto se guarda en el dispositivo (sin cuenta). Escribir contactos es un permiso sensible: tu ficha de Play Store debe declararlo en el formulario de seguridad de los datos y en la política de privacidad.
getContact puente#
window.WebToApk.getContact(requestId: String, contactId: String)
Todo es asíncrono: las lecturas del cursor pueden ser de miles de filas, y un retorno bloqueante de @JavascriptInterface las ejecutaría en el hilo de JS, provocando un ANR. Los resultados llegan como `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pasa una CADENA ('pick-' + Date.now()). Un NÚMERO sin procesar también funciona: el puente lo convierte a través de un double ("1.787491234567E12"), y cada respuesta lo canoniza de nuevo (BridgeRequestId), así que `res.requestId === Date.now()` compara como verdadero en ambos casos. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- NO necesita permiso (selector del sistema) WebToApk.addContact(id, json) <- necesita el permiso de escritura de Contactos
Se describe en su grupo, Contactos, y no por separado.
Ejemplo
Vuelve a leer un contacto por su id (el id de listContacts o searchContacts).
Devuelve: nada por ahora. La respuesta es el evento appmint:contacts (y window.onAppMintContacts) con detail = { requestId, contact: { id, displayName, photoUri, phones: [{ number, type }], emails: [...] } }. contact es null cuando ese contacto ya no existe. En caso de fallo: { requestId, error }. Requiere: activar Contactos en el Paso 4 (Acceso) al compilar.
Actualiza un contacto guardado usando el hook de la página en lugar de un listener de eventos.
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));
}Actualiza una lista guardada de ids de contactos favoritos y elimina los que se hayan borrado del teléfono desde entonces:
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);
}Comprueba si el número que tu app guardó de un cliente sigue en su tarjeta de contacto:
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));
}Notas: window.onAppMintContacts recibe todas las respuestas de contactos (lista, búsqueda, selección, añadir), así que comprueba siempre requestId. Puedes usarlo o addEventListener('appmint:contacts', ...): ambos se disparan. Errores: not_enabled, permission_denied, foreign_origin. El id de pickContact es un id de fila de teléfono, no un id de contacto, así que aquí no sirve. Los contactos son datos personales: tu ficha de Play Store debe declararlos en el formulario de seguridad de los datos y en la política de privacidad.
listContacts puente#
window.WebToApk.listContacts(requestId: String, limit: Int, offset: Int)
Todo es asíncrono: las lecturas del cursor pueden ser de miles de filas, y un retorno bloqueante de @JavascriptInterface las ejecutaría en el hilo de JS, provocando un ANR. Los resultados llegan como `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pasa una CADENA ('pick-' + Date.now()). Un NÚMERO sin procesar también funciona: el puente lo convierte a través de un double ("1.787491234567E12"), y cada respuesta lo canoniza de nuevo (BridgeRequestId), así que `res.requestId === Date.now()` compara como verdadero en ambos casos. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- NO necesita permiso (selector del sistema) WebToApk.addContact(id, json) <- necesita el permiso de escritura de Contactos
Se describe en su grupo, Contactos, y no por separado.
Ejemplo
Lee la agenda del teléfono, página por página, ordenada de la A a la Z por nombre.
Devuelve: nada por ahora. La respuesta llega después como el evento appmint:contacts (y window.onAppMintContacts) con detail = { requestId, contacts: [{ id, displayName, photoUri, phones: [{ number, type }], emails: ['[email protected]'] }] }, o { requestId, error }. Requiere: activar Contactos en el Paso 4 (Acceso) al compilar; al usuario se le pide permiso en la primera llamada.
Carga los primeros 100 contactos. Haz coincidir la respuesta por tu cuenta 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
}Página siguiente. Pasa el número que ya tienes como offset: WebToApk.listContacts(id, 100, 100).
Carga toda la agenda en páginas de 500 hasta que una página corta indique que llegaste al final:
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
}
}Crea una lista de "Invitar amigos" solo con números móviles, e indica al usuario qué hacer cuando se rechaza la lectura:
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);
}Notas: limit se limita a 1-2000. Errores: not_enabled (Contactos estaba desactivado al compilar), permission_denied (el usuario dijo que no), foreign_origin (se llamó desde una página que no es la propia de tu app). El type del teléfono es home, mobile, work, main, work_fax, home_fax o other. photoUri es una dirección content://, no una URL de imagen que una página pueda cargar. Para que el usuario elija solo a una persona, usa pickContact: no necesita permiso. Los contactos son datos personales: tu ficha de Play Store debe declararlos en el formulario de seguridad de los datos y en la política de privacidad.
pickContact puente#
window.WebToApk.pickContact(requestId: String)
Selector de contactos del sistema: NO necesita permiso y es la opciĂłn adecuada siempre que la pĂĄgina solo quiera que el usuario elija a una persona. Elige de la tabla PHONE, no de la tabla de contactos: el selector concede acceso de lectura solo a la fila que devuelve, asĂ que elegir una fila de telĂŠfono es lo que permite leer el nĂşmero sin READ_CONTACTS. Elegir un contacto a secas devolverĂa una fila con nombre y sin nĂşmero, y leer el nĂşmero de ella requerirĂa el permiso que este mĂŠtodo existe para evitar.
Ejemplo
Abre el selector de contactos propio de Android para que el usuario elija un número de teléfono. No se necesita permiso.
Devuelve: nada por ahora. La respuesta es el evento appmint:contacts (y window.onAppMintContacts) con detail = { requestId, contact: { id, displayName, photoUri, phones: [{ number, type: 'picked' }], emails: [] } }. Si el usuario cierra el selector: { requestId, error: 'cancelled' }. Requiere: nada: ni interruptor de compilación ni solicitud de permiso.
Rellena un formulario con la persona elegida.
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);
}Mantén una lista corta de contactos de emergencia. Guarda el nombre y el número, no el id, para que la lista no dependa de la agenda:
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);
});Desactiva el botón mientras el selector está abierto y muestra los campos de escritura cuando el teléfono no tiene app de contactos:
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);
});Notas: Obtienes el nombre y el ÚNICO número que el usuario tocó, nunca correos ni el resto de la agenda. emails siempre está vacío. Errores: cancelled, no_picker (no hay app de contactos en el teléfono). Si la lectura falla tras una selección, contact es null. Como no se lee nada sin la elección del usuario, esta es la forma amigable con Play de obtener un número de teléfono; prefiérela a listContacts cuando solo necesites a una persona. Para apps creadas con IA e importaciones de Google AI Studio, la compilación analiza tu código; una llamada a pickContact( no activa Contactos (solo lo hacen listContacts, searchContacts y getContact), así que el selector no añade ningún permiso de lectura de contactos.
searchContacts puente#
window.WebToApk.searchContacts(requestId: String, query: String, limit: Int)
Todo es asíncrono: las lecturas del cursor pueden ser de miles de filas, y un retorno bloqueante de @JavascriptInterface las ejecutaría en el hilo de JS, provocando un ANR. Los resultados llegan como `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pasa una CADENA ('pick-' + Date.now()). Un NÚMERO sin procesar también funciona: el puente lo convierte a través de un double ("1.787491234567E12"), y cada respuesta lo canoniza de nuevo (BridgeRequestId), así que `res.requestId === Date.now()` compara como verdadero en ambos casos. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- NO necesita permiso (selector del sistema) WebToApk.addContact(id, json) <- necesita el permiso de escritura de Contactos
Se describe en su grupo, Contactos, y no por separado.
Ejemplo
Encuentra contactos cuyo nombre coincide con una palabra de búsqueda, sin cargar toda la agenda.
Devuelve: nada por ahora. La respuesta es el evento appmint:contacts (y window.onAppMintContacts) con detail = { requestId, contacts: [...] } (la misma forma de contacto que listContacts) o { requestId, error }. Requiere: activar Contactos en el Paso 4 (Acceso) al compilar; al usuario se le pide permiso en la primera llamada.
Busca mientras el usuario escribe. Solo se muestra la respuesta a la última búsqueda.
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);
});Envuelve la búsqueda en una Promise y espera hasta que el usuario deje de escribir, para que escribir rápido no inicie una búsqueda por cada tecla:
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);
});Rellena el campo de correo desde la agenda cuando el nombre escrito coincide con una sola persona que tiene correo:
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());
});Notas: Una palabra de búsqueda vacía devuelve contactos desde el principio de la lista (como listContacts). limit se limita a 1-2000 y no hay desplazamiento. Errores: not_enabled, permission_denied, foreign_origin. Los contactos son datos personales: tu ficha de Play Store debe declararlos en el formulario de seguridad de los datos y en la política de privacidad.