Создать APK бесплатно

JavaScript bridge API

Контакты

Чтение, поиск, выбор и запись — для выбора контакта разрешение вообще не нужно.

navigator.contacts веб-стандарт#

navigator.contacts.select(properties, options) — Contact Picker

Пример

Стандартный Contact Picker API: открывается собственный выбор контактов телефона, и страница получает только выбранного пользователем человека — без разрешения на контакты.

Возвращает: Promise с массивом: [{ name: [string], tel: [string] }] или [], если пользователь закрыл окно выбора. Требуется: ничего включать не нужно; вызывайте по нажатию (нужно действие пользователя, иначе SecurityError).

document.getElementById('pick').addEventListener('click', async () => {
  if (!('contacts' in navigator)) { showManualEntry(); return; }
  const props = await navigator.contacts.getProperties();   // ['name', 'tel']
  const [person] = await navigator.contacts.select(props, { multiple: false });
  if (!person) return;                                       // closed without choosing
  document.getElementById('name').value = person.name[0] || '';
  document.getElementById('phone').value = person.tel[0] || '';
});

Заполните поле «Кому» только номером и покажите сообщение для каждого случая, когда выбор контакта может быть отклонён:

document.getElementById('choose-number').addEventListener('click', async () => {
  if (!('contacts' in navigator)) return;
  try {
    const [person] = await navigator.contacts.select(['tel']);
    if (person && person.tel.length) document.getElementById('to').value = person.tel[0];
  } catch (err) {
    if (err.name === 'InvalidStateError') showMessage('The contact picker is already open or not available.');
    else if (err.name === 'SecurityError') showMessage('Tap the button to choose a contact.');
    else showMessage('Could not pick a contact: ' + err.message);
  }
});

Составьте список гостей по одному нажатию (по одному человеку за выбор), пропуская номер, который уже есть в списке:

const guests = [];

document.getElementById('add-guest').addEventListener('click', async () => {
  if (!('contacts' in navigator)) { showManualEntry(); return; }
  const [person] = await navigator.contacts.select(['name', 'tel']);
  if (!person || !person.tel.length) return;   // closed, or no number

  const number = person.tel[0];
  if (guests.some((g) => g.number === number)) { showMessage('Already invited'); return; }
  guests.push({ name: person.name[0] || number, number });
  renderGuests(guests);
});

Примечания: приложение поддерживает name и tel — номер, на который нажал пользователь. email, address и icon потребовали бы полного разрешения на контакты, поэтому они не предлагаются, и запрос на них завершается отказом с TypeError. Один человек за выбор (multiple разрешён, но выбор в Android возвращает одного). Работает только на верхней странице. Для списка или поиска по всем контактам используйте WebToApk.listContacts (нужен переключатель «Контакты»).

addContact мост#

window.WebToApk.addContact(requestId: String, contactJson: String)

Системный выбор (разрешение не нужно) и запись нового контакта (WRITE_CONTACTS). Чтение — список и поиск — описано в группе «Контакты» выше.

Описывается в своей группе, Контакты: выбор и добавление, а не отдельно.

Пример

Сохраняет новый контакт прямо в адресную книгу телефона, не открывая приложение «Контакты».

Возвращает: сейчас ничего. Ответ приходит в событии appmint:contacts (и в window.onAppMintContacts) с detail = { requestId, ok: true, id: '123' } при успехе, { requestId, ok: false, id: null }, если сохранить не удалось, или { requestId, error }. Требуется: при сборке включите Редактирование контактов на шаге 4 (Доступ); при первом вызове у пользователя запрашивается разрешение.

Сохраните клиента. Контакт передаётся в виде строки 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]
  }));
}

Краткая форма. Одиночные значения тоже принимаются: { name: 'Ali', phone: '+911234567890', email: '[email protected]' }.

Кнопка «Сохранить в контакты» на странице визитки, в краткой форме и с 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);
    });
});

Импортируйте список команды по одному человеку, с рабочими и домашними номерами, и сообщите, сколько сохранено:

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');
}

Примечания: Ошибки: not_enabled, permission_denied. Возвращаемый id — это новый идентификатор raw-contact. Телефон с type, отличным от home или work, сохраняется как mobile. Контакт сохраняется на устройстве (без аккаунта). Запись контактов — это чувствительное разрешение: ваша карточка в Play Store должна указывать его в форме Data safety и в политике конфиденциальности.

getContact мост#

window.WebToApk.getContact(requestId: String, contactId: String)

Всё асинхронно: чтение курсора может занимать тысячи строк, а блокирующий возврат @JavascriptInterface выполнял бы это в потоке JS — получился бы ANR. Результаты приходят как `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: передавайте СТРОКУ ('pick-' + Date.now()). Обычное ЧИСЛО тоже работает — мост приводит его через double ("1.787491234567E12"), а каждый ответ приводит его обратно (BridgeRequestId), так что строгое сравнение `res.requestId === Date.now()` в обоих случаях даёт true. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- разрешение НЕ нужно (системный выбор) WebToApk.addContact(id, json) <- нужно разрешение на запись контактов

Описывается в своей группе, Контакты, а не отдельно.

Пример

Заново читает один контакт по его id (id из listContacts или searchContacts).

Возвращает: сейчас ничего. Ответ приходит в событии appmint:contacts (и в window.onAppMintContacts) с detail = { requestId, contact: { id, displayName, photoUri, phones: [{ number, type }], emails: [...] } }. contact равно null, если такого контакта больше нет. При сбое: { requestId, error }. Требуется: при сборке включите Контакты на шаге 4 (Доступ).

Обновите сохранённый контакт, используя хук страницы вместо слушателя событий.

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));
}

Обновите сохранённый список идентификаторов избранных контактов и уберите те, что с тех пор удалены с телефона:

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);
}

Проверьте, остался ли номер, сохранённый вашим приложением для клиента, в его карточке контакта:

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));
}

Примечания: window.onAppMintContacts получает каждый ответ по контактам (список, поиск, выбор, добавление), поэтому всегда проверяйте requestId. Можно использовать его или addEventListener('appmint:contacts', ...) — сработают оба. Ошибки: not_enabled, permission_denied, foreign_origin. id из pickContact — это идентификатор строки телефона, а не идентификатор контакта, поэтому здесь он не подходит. Контакты — это личные данные: ваша карточка в Play Store должна указывать их в форме Data safety и в политике конфиденциальности.

listContacts мост#

window.WebToApk.listContacts(requestId: String, limit: Int, offset: Int)

Всё асинхронно: чтение курсора может занимать тысячи строк, а блокирующий возврат @JavascriptInterface выполнял бы это в потоке JS — получился бы ANR. Результаты приходят как `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: передавайте СТРОКУ ('pick-' + Date.now()). Обычное ЧИСЛО тоже работает — мост приводит его через double ("1.787491234567E12"), а каждый ответ приводит его обратно (BridgeRequestId), так что строгое сравнение `res.requestId === Date.now()` в обоих случаях даёт true. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- разрешение НЕ нужно (системный выбор) WebToApk.addContact(id, json) <- нужно разрешение на запись контактов

Описывается в своей группе, Контакты, а не отдельно.

Пример

Читает адресную книгу телефона постранично, в порядке имён от А до Я.

Возвращает: сейчас ничего. Ответ приходит позже в событии appmint:contacts (и в window.onAppMintContacts) с detail = { requestId, contacts: [{ id, displayName, photoUri, phones: [{ number, type }], emails: ['[email protected]'] }] } или { requestId, error }. Требуется: при сборке включите Контакты на шаге 4 (Доступ); при первом вызове у пользователя запрашивается разрешение.

Загрузите первые 100 контактов. Сопоставьте ответ по своему 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
}

Следующая страница. Передайте уже имеющееся число как offset: WebToApk.listContacts(id, 100, 100).

Загрузите всю адресную книгу страницами по 500, пока короткая страница не покажет, что вы дошли до конца:

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
  }
}

Составьте список «Пригласить друзей» только из мобильных номеров и подскажите пользователю, что делать, если чтение отклонено:

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);
}

Примечания: limit ограничивается диапазоном 1–2000. Ошибки: not_enabled (при сборке «Контакты» были выключены), permission_denied (пользователь отказал), foreign_origin (вызов со страницы, которая не принадлежит вашему приложению). Тип телефона type: home, mobile, work, main, work_fax, home_fax или other. photoUri — это адрес content://, а не URL изображения, который страница может загрузить. Чтобы дать пользователю выбрать только одного человека, используйте pickContact — ему не нужно разрешение. Контакты — это личные данные: ваша карточка в Play Store должна указывать их в форме Data safety и в политике конфиденциальности.

pickContact мост#

window.WebToApk.pickContact(requestId: String)

Системный выбор контакта — НЕ требует разрешения и подходит всякий раз, когда странице нужно просто дать пользователю выбрать одного человека. Выбирает из таблицы PHONE, а не из таблицы контактов: выбор даёт доступ на чтение только к возвращённой строке, поэтому выбор строки с телефоном — это то, что делает номер доступным без READ_CONTACTS. Выбор «голого» контакта вернул бы строку с именем без номера, а чтение номера из неё потребовало бы разрешения, которого этот метод как раз позволяет избежать.

Пример

Открывает собственный выбор контактов Android, чтобы пользователь выбрал один номер телефона. Разрешение не нужно.

Возвращает: сейчас ничего. Ответ приходит в событии appmint:contacts (и в window.onAppMintContacts) с detail = { requestId, contact: { id, displayName, photoUri, phones: [{ number, type: 'picked' }], emails: [] } }. Если пользователь закрыл окно выбора: { requestId, error: 'cancelled' }. Требуется: ничего — ни переключателя при сборке, ни запроса разрешения.

Заполните форму данными выбранного человека.

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);
}

Ведите короткий список экстренных контактов. Сохраняйте имя и номер, а не id, чтобы список не зависел от адресной книги:

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);
});

Отключайте кнопку, пока открыт выбор, и показывайте поля для ввода, если на телефоне нет приложения контактов:

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);
});

Примечания: Вы получаете имя и ОДИН номер, на который нажал пользователь, — никогда адреса почты и не остальную книгу. emails всегда пуст. Ошибки: cancelled, no_picker (на телефоне нет приложения контактов). Если чтение после выбора не удалось, contact равно null. Поскольку без выбора пользователя ничего не читается, это способ получить номер телефона, удобный для Play; предпочитайте его listContacts, когда нужен только один человек. Для приложений, созданных ИИ, и импортов из Google AI Studio сборка сканирует ваш код; вызов pickContact( не включает Контакты — это делают только listContacts, searchContacts и getContact — поэтому выбор контакта не добавляет разрешение на чтение контактов.

searchContacts мост#

window.WebToApk.searchContacts(requestId: String, query: String, limit: Int)

Всё асинхронно: чтение курсора может занимать тысячи строк, а блокирующий возврат @JavascriptInterface выполнял бы это в потоке JS — получился бы ANR. Результаты приходят как `appmint:contacts` / window.onAppMintContacts({requestId,...}). requestId: передавайте СТРОКУ ('pick-' + Date.now()). Обычное ЧИСЛО тоже работает — мост приводит его через double ("1.787491234567E12"), а каждый ответ приводит его обратно (BridgeRequestId), так что строгое сравнение `res.requestId === Date.now()` в обоих случаях даёт true. WebToApk.listContacts(id, limit, offset) WebToApk.searchContacts(id, query, limit) WebToApk.getContact(id, contactId) WebToApk.pickContact(id) <- разрешение НЕ нужно (системный выбор) WebToApk.addContact(id, json) <- нужно разрешение на запись контактов

Описывается в своей группе, Контакты, а не отдельно.

Пример

Находит контакты, имя которых совпадает с поисковым словом, не загружая всю адресную книгу.

Возвращает: сейчас ничего. Ответ приходит в событии appmint:contacts (и в window.onAppMintContacts) с detail = { requestId, contacts: [...] } — тот же формат контакта, что у listContacts — или { requestId, error }. Требуется: при сборке включите Контакты на шаге 4 (Доступ); при первом вызове у пользователя запрашивается разрешение.

Ищите по мере ввода. Показывается только ответ на последний поиск.

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);
});

Оберните поиск в Promise и ждите, пока пользователь перестанет печатать, чтобы быстрый набор не запускал поиск на каждую клавишу:

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);
});

Заполните поле почты из адресной книги, когда введённое имя совпадает с одним человеком, у которого есть адрес почты:

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());
});

Примечания: Пустое слово поиска возвращает контакты с начала списка (как listContacts). limit ограничивается диапазоном 1–2000, смещения нет. Ошибки: not_enabled, permission_denied, foreign_origin. Контакты — это личные данные: ваша карточка в Play Store должна указывать их в форме Data safety и в политике конфиденциальности.