JavaScript bridge API
Contacts
Reading, searching, picking and writing - the picker needs no permission at all.
addContact bridge#
window.WebToApk.addContact(requestId: String, contactJson: String)
The system picker (no permission needed) and writing a new contact (WRITE_CONTACTS). The read side - listing and searching - is under the Contacts group above.
Described by its group, Contacts: pick and add, rather than on its own.
Example
Saves a new contact straight into the phone's address book, without opening the Contacts app.
Returns: nothing now. The answer is the appmint:contacts event (and window.onAppMintContacts) with detail = { requestId, ok: true, id: '123' } on success, { requestId, ok: false, id: null } when the save failed, or { requestId, error }. Needs: turn on Edit Contacts in Step 4 (Access) when you build; the user is asked for permission on the first call.
Save a customer. The contact is passed as a JSON string.
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]
}));
}Short form. Single values are accepted too: { name: 'Ali', phone: '+911234567890', email: '[email protected]' }.
Notes: Errors: not_enabled, permission_denied. The returned id is the new raw-contact id. A phone type other than home or work is saved as mobile. The contact is saved on the device (no account). Writing contacts is a sensitive permission: your Play Store listing must declare it in the Data safety form and privacy policy.
getContact bridge#
window.WebToApk.getContact(requestId: String, contactId: String)
All async: cursor reads can be thousands of rows, and a blocking @JavascriptInterface return would run that on the JS thread - an ANR. Results arrive as `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pass a STRING ('pick-' + Date.now()). A raw NUMBER also works - the bridge coerces it through a double ("1.787491234567E12"), and every reply canonicalizes it back (BridgeRequestId), so `res.requestId === Date.now()` strict-compares true either way. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- NO permission needed (system picker) WebToApk.addContact(id, json) <- needs the Contacts-write permission
Described by its group, Contacts, rather than on its own.
Example
Reads one contact again by its id (the id from listContacts or searchContacts).
Returns: nothing now. The answer is the appmint:contacts event (and window.onAppMintContacts) with detail = { requestId, contact: { id, displayName, photoUri, phones: [{ number, type }], emails: [...] } }. contact is null when that contact no longer exists. On failure: { requestId, error }. Needs: turn on Contacts in Step 4 (Access) when you build.
Refresh a saved contact using the page hook instead of an event listener.
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));
}Notes: window.onAppMintContacts receives every contacts answer (list, search, pick, add), so always check requestId. You can use it or addEventListener('appmint:contacts', ...) - both fire. Errors: not_enabled, permission_denied, foreign_origin. The id from pickContact is a phone-row id, not a contact id, so it does not work here. Contacts is personal data: your Play Store listing must declare it in the Data safety form and privacy policy.
listContacts bridge#
window.WebToApk.listContacts(requestId: String, limit: Int, offset: Int)
All async: cursor reads can be thousands of rows, and a blocking @JavascriptInterface return would run that on the JS thread - an ANR. Results arrive as `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pass a STRING ('pick-' + Date.now()). A raw NUMBER also works - the bridge coerces it through a double ("1.787491234567E12"), and every reply canonicalizes it back (BridgeRequestId), so `res.requestId === Date.now()` strict-compares true either way. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- NO permission needed (system picker) WebToApk.addContact(id, json) <- needs the Contacts-write permission
Described by its group, Contacts, rather than on its own.
Example
Reads the phone's address book, page by page, sorted A to Z by name.
Returns: nothing now. The answer comes later as the appmint:contacts event (and window.onAppMintContacts) with detail = { requestId, contacts: [{ id, displayName, photoUri, phones: [{ number, type }], emails: ['[email protected]'] }] }, or { requestId, error }. Needs: turn on Contacts in Step 4 (Access) when you build; the user is asked for permission on the first call.
Load the first 100 contacts. Match the answer on your own 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
}Next page. Pass the number you already have as offset: WebToApk.listContacts(id, 100, 100).
Notes: limit is clamped to 1-2000. Errors: not_enabled (Contacts was off at build time), permission_denied (the user said No), foreign_origin (called from a page that is not your app's own). Phone type is home, mobile, work, main, work_fax, home_fax or other. photoUri is a content:// address, not an image URL a page can load. To let the user choose just one person, use pickContact instead - it needs no permission. Contacts is personal data: your Play Store listing must declare it in the Data safety form and privacy policy.
pickContact bridge#
window.WebToApk.pickContact(requestId: String)
System contact picker - needs NO permission, and is the right choice whenever the page just wants the user to choose one person. Picks from the PHONE table, not the contact table: the picker grants read access only to the row it returns, so picking a phone row is what makes the number readable without READ_CONTACTS. Picking a bare contact would return a row with a name and no number, and reading the number from it would need the permission this method exists to avoid.
Example
Opens Android's own contact picker so the user chooses one phone number. No permission is needed.
Returns: nothing now. The answer is the appmint:contacts event (and window.onAppMintContacts) with detail = { requestId, contact: { id, displayName, photoUri, phones: [{ number, type: 'picked' }], emails: [] } }. If the user closes the picker: { requestId, error: 'cancelled' }. Needs: nothing - no build switch, no permission prompt.
Fill a form with the chosen person.
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);
}Notes: You get the name and the ONE number the user tapped - never emails, never the rest of the book. emails is always empty. Errors: cancelled, no_picker (no contacts app on the phone). If the read fails after a pick, contact is null. Because nothing is read without the user's choice, this is the Play-friendly way to get a phone number; prefer it over listContacts when you only need one person. For AI-built apps and Google AI Studio imports the build scans your code; a pickContact( call does not tick Contacts - only listContacts, searchContacts and getContact do - so the picker adds no contacts-read permission.
searchContacts bridge#
window.WebToApk.searchContacts(requestId: String, query: String, limit: Int)
All async: cursor reads can be thousands of rows, and a blocking @JavascriptInterface return would run that on the JS thread - an ANR. Results arrive as `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: pass a STRING ('pick-' + Date.now()). A raw NUMBER also works - the bridge coerces it through a double ("1.787491234567E12"), and every reply canonicalizes it back (BridgeRequestId), so `res.requestId === Date.now()` strict-compares true either way. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- NO permission needed (system picker) WebToApk.addContact(id, json) <- needs the Contacts-write permission
Described by its group, Contacts, rather than on its own.
Example
Finds contacts whose name matches a search word, without loading the whole address book.
Returns: nothing now. The answer is the appmint:contacts event (and window.onAppMintContacts) with detail = { requestId, contacts: [...] } - the same contact shape as listContacts - or { requestId, error }. Needs: turn on Contacts in Step 4 (Access) when you build; the user is asked for permission on the first call.
Search as the user types. Only the answer to the latest search is shown.
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);
});Notes: An empty search word returns contacts from the start of the list (like listContacts). limit is clamped to 1-2000 and there is no offset. Errors: not_enabled, permission_denied, foreign_origin. Contacts is personal data: your Play Store listing must declare it in the Data safety form and privacy policy.