API da ponte JavaScript
Contatos
Ler, buscar, escolher e gravar — o seletor não precisa de nenhuma permissão.
addContact ponte#
window.WebToApk.addContact(requestId: String, contactJson: String)
O seletor do sistema (sem permissão) e a gravação de um novo contato (WRITE_CONTACTS). O lado de leitura — listar e buscar — está no grupo Contatos acima.
Descrito pelo seu grupo, Contatos: escolher e adicionar, e não separadamente.
Exemplo
Salva um novo contato direto na agenda do telefone, sem abrir o app Contatos.
Retorna: nada agora. A resposta é o evento appmint:contacts (e window.onAppMintContacts) com detail = { requestId, ok: true, id: '123' } em caso de sucesso, { requestId, ok: false, id: null } quando o salvamento falhou, ou { requestId, error }. Requer: ative Editar contatos na Etapa 4 (Acesso) ao compilar; o usuário é solicitado a dar permissão na primeira chamada.
Salve um cliente. O contato é passado como uma string 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 curta. Valores únicos também são aceitos: { name: 'Ali', phone: '+911234567890', email: '[email protected]' }.
Um botão "Salvar nos contatos" em uma página de cartão de visita, usando a forma curta e uma 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);
});
});Importe a lista de uma equipe, uma pessoa por vez, com números de trabalho e de casa, e informe quantos foram salvos:
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');
}Observações: Erros: not_enabled, permission_denied. O id retornado é o novo id de contato bruto. Um type de telefone diferente de home ou work é salvo como mobile. O contato é salvo no dispositivo (sem conta). Gravar contatos é uma permissão sensível: sua página na Play Store deve declará-la no formulário de Segurança dos dados e na política de privacidade.
getContact ponte#
window.WebToApk.getContact(requestId: String, contactId: String)
Tudo assíncrono: a leitura do cursor pode ter milhares de linhas, e um retorno bloqueante de @JavascriptInterface executaria isso na thread do JS — um ANR. Os resultados chegam como `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: passe uma STRING ('pick-' + Date.now()). Um NÚMERO puro também funciona — a bridge o converte por um double ("1.787491234567E12"), e toda resposta o canonicaliza de volta (BridgeRequestId), então `res.requestId === Date.now()` com comparação estrita dá true nos dois casos. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- NÃO precisa de permissão (seletor do sistema) WebToApk.addContact(id, json) <- precisa da permissão de escrita de Contatos
Descrito pelo seu grupo, Contatos, e não separadamente.
Exemplo
Lê um contato novamente pelo seu id (o id de listContacts ou searchContacts).
Retorna: nada agora. A resposta é o evento appmint:contacts (e window.onAppMintContacts) com detail = { requestId, contact: { id, displayName, photoUri, phones: [{ number, type }], emails: [...] } }. contact é null quando esse contato não existe mais. Em caso de falha: { requestId, error }. Requer: ative Contatos na Etapa 4 (Acesso) ao compilar.
Atualize um contato salvo usando o hook da página em vez de um listener de evento.
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));
}Atualize uma lista salva de ids de contatos favoritos e remova os que foram apagados do telefone desde então:
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);
}Verifique se o número que seu app guardou para um cliente ainda está no cartão de contato dele:
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));
}Observações: window.onAppMintContacts recebe todas as respostas de contatos (listar, buscar, escolher, adicionar), então sempre verifique requestId. Você pode usá-lo ou addEventListener('appmint:contacts', ...) — ambos disparam. Erros: not_enabled, permission_denied, foreign_origin. O id de pickContact é um id de linha de telefone, não um id de contato, então não funciona aqui. Contatos são dados pessoais: sua página na Play Store deve declarar isso no formulário de Segurança dos dados e na política de privacidade.
listContacts ponte#
window.WebToApk.listContacts(requestId: String, limit: Int, offset: Int)
Tudo assíncrono: a leitura do cursor pode ter milhares de linhas, e um retorno bloqueante de @JavascriptInterface executaria isso na thread do JS — um ANR. Os resultados chegam como `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: passe uma STRING ('pick-' + Date.now()). Um NÚMERO puro também funciona — a bridge o converte por um double ("1.787491234567E12"), e toda resposta o canonicaliza de volta (BridgeRequestId), então `res.requestId === Date.now()` com comparação estrita dá true nos dois casos. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- NÃO precisa de permissão (seletor do sistema) WebToApk.addContact(id, json) <- precisa da permissão de escrita de Contatos
Descrito pelo seu grupo, Contatos, e não separadamente.
Exemplo
Lê a agenda do telefone, página por página, em ordem alfabética (A a Z) por nome.
Retorna: nada agora. A resposta vem depois como o evento appmint:contacts (e window.onAppMintContacts) com detail = { requestId, contacts: [{ id, displayName, photoUri, phones: [{ number, type }], emails: ['[email protected]'] }] }, ou { requestId, error }. Requer: ative Contatos na Etapa 4 (Acesso) ao compilar; o usuário é solicitado a dar permissão na primeira chamada.
Carregue os primeiros 100 contatos. Relacione a resposta por conta própria 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
}Próxima página. Passe o número que você já tem como offset: WebToApk.listContacts(id, 100, 100).
Carregue a agenda inteira em páginas de 500 até que uma página menor indique que você chegou ao fim:
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
}
}Monte uma lista "Convidar amigos" só com números de celular e diga ao usuário o que fazer quando a leitura for recusada:
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);
}Observações: limit é limitado a 1-2000. Erros: not_enabled (Contatos estava desativado na compilação), permission_denied (o usuário disse Não), foreign_origin (chamado de uma página que não é do próprio app). O type do telefone é home, mobile, work, main, work_fax, home_fax ou other. photoUri é um endereço content://, não uma URL de imagem que uma página consiga carregar. Para deixar o usuário escolher apenas uma pessoa, use pickContact — não precisa de permissão. Contatos são dados pessoais: sua página na Play Store deve declarar isso no formulário de Segurança dos dados e na política de privacidade.
pickContact ponte#
window.WebToApk.pickContact(requestId: String)
Seletor de contatos do sistema - NĂO precisa de permissĂŁo e Ă© a escolha certa sempre que a pĂĄgina sĂł quer que o usuĂĄrio escolha uma pessoa. Escolhe da tabela PHONE, nĂŁo da tabela de contatos: o seletor concede acesso de leitura apenas Ă linha que ele retorna, e escolher uma linha de telefone Ă© o que torna o nĂșmero legĂvel sem READ_CONTACTS. Escolher um contato simples retornaria uma linha com nome e sem nĂșmero, e ler o nĂșmero a partir dela exigiria a permissĂŁo que este mĂ©todo existe para evitar.
Exemplo
Abre o seletor de contatos do próprio Android para o usuário escolher um número de telefone. Não precisa de permissão.
Retorna: nada agora. A resposta é o evento appmint:contacts (e window.onAppMintContacts) com detail = { requestId, contact: { id, displayName, photoUri, phones: [{ number, type: 'picked' }], emails: [] } }. Se o usuário fechar o seletor: { requestId, error: 'cancelled' }. Requer: nada — sem chave de compilação, sem pedido de permissão.
Preencha um formulário com a pessoa escolhida.
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);
}Mantenha uma lista curta de contatos de emergência. Salve o nome e o número, não o id, para que a lista não dependa da 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);
});Desative o botão enquanto o seletor estiver aberto e mostre os campos de digitação quando o telefone não tiver app de contatos:
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);
});Observações: Você recebe o nome e o ÚNICO número em que o usuário tocou — nunca e-mails, nunca o resto da agenda. emails é sempre vazio. Erros: cancelled, no_picker (sem app de contatos no telefone). Se a leitura falhar após a seleção, contact é null. Como nada é lido sem a escolha do usuário, esta é a forma amigável ao Play de obter um número de telefone; prefira-a a listContacts quando você só precisa de uma pessoa. Para apps criados com IA e importações do Google AI Studio, a compilação analisa seu código; uma chamada pickContact( não marca Contatos — só listContacts, searchContacts e getContact marcam — então o seletor não adiciona permissão de leitura de contatos.
searchContacts ponte#
window.WebToApk.searchContacts(requestId: String, query: String, limit: Int)
Tudo assíncrono: a leitura do cursor pode ter milhares de linhas, e um retorno bloqueante de @JavascriptInterface executaria isso na thread do JS — um ANR. Os resultados chegam como `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: passe uma STRING ('pick-' + Date.now()). Um NÚMERO puro também funciona — a bridge o converte por um double ("1.787491234567E12"), e toda resposta o canonicaliza de volta (BridgeRequestId), então `res.requestId === Date.now()` com comparação estrita dá true nos dois casos. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- NÃO precisa de permissão (seletor do sistema) WebToApk.addContact(id, json) <- precisa da permissão de escrita de Contatos
Descrito pelo seu grupo, Contatos, e não separadamente.
Exemplo
Encontra contatos cujo nome corresponde a uma palavra de busca, sem carregar a agenda inteira.
Retorna: nada agora. A resposta é o evento appmint:contacts (e window.onAppMintContacts) com detail = { requestId, contacts: [...] } — o mesmo formato de contato de listContacts — ou { requestId, error }. Requer: ative Contatos na Etapa 4 (Acesso) ao compilar; o usuário é solicitado a dar permissão na primeira chamada.
Busque enquanto o usuário digita. Só a resposta da busca mais recente é exibida.
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);
});Envolva a busca em uma Promise e espere até o usuário parar de digitar, para que digitar rápido não inicie uma busca por 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);
});Preencha o campo de e-mail a partir da agenda quando o nome digitado corresponder a uma pessoa que tem 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());
});Observações: Uma palavra de busca vazia retorna contatos do início da lista (como listContacts). limit é limitado a 1-2000 e não há offset. Erros: not_enabled, permission_denied, foreign_origin. Contatos são dados pessoais: sua página na Play Store deve declarar isso no formulário de Segurança dos dados e na política de privacidade.