Contacts¶
Choose one phone number or email, look up contacts with permission, or open the system editor to let the person create or edit a contact. Useful for CRM clients, appointment apps and user-selected recipients. ApkPy does not upload the address book or send messages on your behalf.
Smallest complete example¶
from apkpy_lib import Screen, button, contacts, json_get, label, run
home = Screen(id="home")
details = label("Choose a phone number", screen=home)
def picked(ok, value):
if ok:
details.set_value(json_get(value, "name") + ": " + json_get(value, "phone"))
else:
details.set_value("Contacts: " + value)
button("Choose phone", screen=home,
command=lambda: contacts.pick(kind="phone", on_result=picked))
run(start_screen=home)
This app declares neither READ_CONTACTS nor WRITE_CONTACTS. Android opens
its contact picker and delegates access to the chosen phone/email row, not
the whole address book. A contact with several numbers may require another
choice in the system UI. Use kind="email" to select an email instead.
API and callback contract¶
All on_result callbacks receive (ok, value) on the UI thread. On failure,
value is a reason string; on success it depends on the operation below.
| Method | Successful result |
|---|---|
contacts.pick(kind="phone", on_result=None) |
One contact JSON object containing the chosen phone or email |
contacts.list(query="", limit=50, offset=0, on_result=None) |
JSON array of contacts; empty array is a successful empty result |
contacts.get(id, on_result=None) |
One contact JSON object; accepts a numeric id or returned lookup uri |
contacts.create(name="", phone="", email="", on_result=None) |
Editor-return JSON; not proof that the contact was saved |
contacts.edit(id, on_result=None) |
Editor-return JSON; the native app owns save/cancel |
contacts.settings(on_result=None) |
"opened" after opening the app's Android settings |
Row shape:
{"id":"42","uri":"content://com.android.contacts/contacts/lookup/key/42","name":"Alex","phone":"+1 202-555-0101","email":"","phones":["+1 202-555-0101"],"emails":[]}
All seven fields exist. phones and emails contain unique non-empty strings.
The singular fields contain the first entry. The picker returns only the
selected detail: choosing a phone leaves email empty and emails empty,
even if that person has email data. It does not perform a hidden full-contact
read. list and get return phone/email details available to the provider.
Use json_get() for fields and pass the successful list result directly to
virtual_collection.set_items(). Prefer the lookup URI to a numeric id when
storing a selection; contacts can be merged or removed by other applications.
Neither form is a permanent identity credential.
Browse with explicit permission¶
The following snippets extend an app with search input, status label and
people virtual collection. For all declarations together, use
People Desk.
def loaded(ok, value):
if ok:
people.set_items(value)
else:
status.set_value("Contacts: " + value)
def search_people():
contacts.list(query=search.get_value(), limit=25, offset=0, on_result=loaded)
list and get add READ_CONTACTS to the manifest and ask at the time of use.
No permission is requested just because the app starts. Refusal reports
permission_denied; if Android will no longer show the prompt it reports
permission_blocked. Open contacts.settings() only after explaining why
access is needed, then let the person decide. Managed-device policies and
Android's permission history can also prevent prompting.
Search is a literal substring of the display name, using SQLite's ASCII
case-insensitive behavior; it is not a phone/email search or full Unicode
case folding. % and _ are literal characters, not SQL wildcards.
Results sort by display name (NOCASE), then id. limit is 1–100 and offset
is 0–100000. Changes made between pages can shift offsets; refresh after edits.
Queries run on a dedicated background executor. Each non-empty page uses a base contact query and one batched phone/email query, not one query per row. Pagination skips through a provider cursor; very large offsets are not a constant-time database cursor API. No photos, notes, groups or accounts are read.
To request a later page, pass a larger offset and append the result instead
of replacing the collection. Here is the callback for a second 25-item page:
def next_page_loaded(ok, value):
if ok:
people.append_items(value, has_more=False)
else:
people.finish_load(has_more=True)
status.set_value("Contacts: " + value)
def load_second_page():
contacts.list(query=search.get_value(), limit=25, offset=25,
on_result=next_page_loaded)
This example deliberately stops after two pages. In a longer directory, keep the offset and end-of-results policy in your app; no total count or next-page cursor is returned. Reset the offset when changing the search query.
Create and edit with the system UI¶
def editor_closed(ok, value):
if ok:
status.set_value("Editor closed. Pick the contact again to verify changes.")
else:
status.set_value("Contacts: " + value)
def new_customer():
contacts.create(name="Example customer", phone="+1 202-555-0101",
email="customer@example.com", on_result=editor_closed)
Creation passes these fields to Android's ACTION_INSERT editor. Editing
opens ACTION_EDIT for the chosen contact. There is no WRITE_CONTACTS
permission and no direct insert/update/delete of the provider. The person
selects the account and chooses whether to save; a synchronized account may
sync changes according to that account's settings.
After either editor returns, success contains:
result_code is "ok" or "cancelled", reflecting Android's activity result.
Some OEM editors save without returning an URI or return cancelled even
after a change. Therefore ok=True means only the editor returned, not
that a write was confirmed. Do not announce "Saved" based on this callback.
Pick or reload the contact to verify. An edit of a stale URI is handled by the
native editor, which may show its own error.
def edit_selected():
contacts.edit(contact_reference.get_value(), on_result=editor_closed)
def reload_selected():
contacts.get(contact_reference.get_value(), on_result=picked)
Here contact_reference is an input containing the selected row's uri, and
picked is the result handler shown above. Reloading calls get and therefore
requires read permission; choosing again with pick does not.
Failures and lifecycle¶
| Reason | Action |
|---|---|
cancelled |
No picker selection; leave the existing value unchanged |
permission_denied |
Explain the read-only use case; offer the scoped picker |
permission_blocked |
Offer app settings, without looping permission prompts |
not_found |
Refresh a stale selection |
unsupported |
No compatible picker/editor/settings activity exists |
busy |
Finish the current contacts operation first |
invalid |
Check kind, contact reference or page bounds |
unavailable |
Use an active foreground screen, not a background job |
unknown |
Inspect the technical log; do not log address-book contents |
One operation may be active per screen. Results wait while the Activity is paused and return on resume. Pending picker/editor/permission operations and callback names are saved for Activity recreation. An interrupted read can be reissued after recreation. Destroyed instances cancel queries and discard old callbacks. Background-job calls are rejected at compile time.
Previewer¶
The themed panel says CONTACTS · SIMULATED. It contains only fictional entries; it never reads or writes your computer's contacts. Pick a row, search, simulate allowing/denying/blocking read access, or save/cancel an in-frame form. Simulated edits persist for the Previewer process and reset on restart. Changing screen closes a simulated panel. The desktop UI tests application flow, not Android's provider, delegated grants, account sync or OEM editors.
Full lab: download People Desk, also
available as examples/30_contacts.py in the repository. The library workspace
copy is playground/contacts_lab/writehere.py.
Limits and privacy¶
No bulk writes, direct deletion, contact observers, cloud synchronization,
groups, photos, vCards, call/SMS sending or default-dialer behavior is included.
For one recipient, use the picker rather than reading the whole address book.
Do not collect or upload contacts merely because the user granted permission.
Apps not using contacts receive no contacts helper; picker/editor-only apps
receive no address-book permission or new Android dependency.
Validation status¶
People Desk reached a real Gradle build and a Python-free APK inspection. The connected-phone check opened and cancelled the native phone picker. No real address-book listing, saving/editing or rotation flow was verified in that check. The simulator cannot certify provider grants or OEM editor results. See 1.9.0 verification for the test counts and remaining checks; this guide is not a publication announcement.
Related: NFC tags, 1.9.0 scope, device API and troubleshooting.
Platform references: contact intents, reading contacts, editing through intents.