@capgo/capacitor-contacts 8.0.19 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -24
- package/android/src/main/java/app/capgo/contacts/CapacitorContactsPlugin.java +246 -20
- package/dist/docs.json +71 -5
- package/dist/esm/definitions.d.ts +74 -0
- package/dist/esm/definitions.js +29 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/plugin.cjs.js +30 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +30 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorContactsPlugin/CapacitorContactsPlugin.swift +190 -56
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,6 +45,48 @@ npm install @capgo/capacitor-contacts
|
|
|
45
45
|
npx cap sync
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
## Android
|
|
49
|
+
|
|
50
|
+
Google Play's Contacts Permissions policy (effective 27 January 2027) allows `READ_CONTACTS` only when the Android Contact Picker cannot cover your core feature, for apps that target Android 17 (API 37) or later.
|
|
51
|
+
|
|
52
|
+
This plugin does **not** declare `READ_CONTACTS` or `WRITE_CONTACTS`. Add them in your app manifest only if you need them.
|
|
53
|
+
|
|
54
|
+
**Pick a phone, email, or address without `READ_CONTACTS`:**
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { CapacitorContacts, ContactProperty } from '@capgo/capacitor-contacts';
|
|
58
|
+
|
|
59
|
+
const { contacts } = await CapacitorContacts.pickContacts({
|
|
60
|
+
property: ContactProperty.PhoneNumber,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const number = contacts[0]?.phoneNumbers?.[0]?.value;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`property` works on every Android version. The picker returns a data-row URI the plugin can read without a permission. The result contains `id`, `displayName`, and the selected property only.
|
|
67
|
+
|
|
68
|
+
Use `ContactProperty.EmailAddress` or `ContactProperty.PostalAddress` the same way.
|
|
69
|
+
|
|
70
|
+
**When you still need `READ_CONTACTS`:**
|
|
71
|
+
|
|
72
|
+
- `getContacts()`, `getContactById()`, `countContacts()`, `getGroups()`, `getAccounts()`
|
|
73
|
+
- Picking a full contact without `property` on Android 16 and below
|
|
74
|
+
- Syncing, backing up, or matching the whole address book
|
|
75
|
+
|
|
76
|
+
If those are core features, add the permission and file the Play Console declaration before pre-review checks start on 27 October 2026:
|
|
77
|
+
|
|
78
|
+
```xml
|
|
79
|
+
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`WRITE_CONTACTS` is unchanged by this policy. Add it only if you create or update contacts in code.
|
|
83
|
+
|
|
84
|
+
On Android 17 and later, picking a full contact without `property` also works without `READ_CONTACTS` because the system picker returns a session URI. Keep using `property` if you still support older Android versions and want the permission out of the manifest entirely.
|
|
85
|
+
|
|
86
|
+
## iOS
|
|
87
|
+
|
|
88
|
+
`pickContact()` / `pickContacts()` never require a contacts permission. Add `NSContactsUsageDescription` only if you call methods that read or write the address book (`getContacts`, `createContact`, and similar).
|
|
89
|
+
|
|
48
90
|
## API
|
|
49
91
|
|
|
50
92
|
<docgen-index>
|
|
@@ -73,6 +115,7 @@ npx cap sync
|
|
|
73
115
|
* [`getPluginVersion()`](#getpluginversion)
|
|
74
116
|
* [Interfaces](#interfaces)
|
|
75
117
|
* [Type Aliases](#type-aliases)
|
|
118
|
+
* [Enums](#enums)
|
|
76
119
|
|
|
77
120
|
</docgen-index>
|
|
78
121
|
|
|
@@ -359,6 +402,9 @@ pickContact(options?: PickContactsOptions | undefined) => Promise<PickContactRes
|
|
|
359
402
|
|
|
360
403
|
<a href="#pick">Pick</a> a single contact using the native contact picker.
|
|
361
404
|
|
|
405
|
+
Same options as {@link pickContacts}. Pass `property` to select one phone
|
|
406
|
+
number, email address, or postal address without `READ_CONTACTS` on Android.
|
|
407
|
+
|
|
362
408
|
| Param | Type | Description |
|
|
363
409
|
| ------------- | ------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
364
410
|
| **`options`** | <code><a href="#pickcontactsoptions">PickContactsOptions</a></code> | - Optional fields to retrieve and picker configuration |
|
|
@@ -378,6 +424,18 @@ pickContacts(options?: PickContactsOptions | undefined) => Promise<PickContactsR
|
|
|
378
424
|
|
|
379
425
|
<a href="#pick">Pick</a> one or more contacts using the native contact picker.
|
|
380
426
|
|
|
427
|
+
On iOS this never requires a contacts permission.
|
|
428
|
+
|
|
429
|
+
On Android, pass `property` to select a single phone number, email address,
|
|
430
|
+
or postal address with no `READ_CONTACTS` permission. That works on every
|
|
431
|
+
Android version and is the way to ship without declaring the permission
|
|
432
|
+
when targeting Android 17 (API 37) or later.
|
|
433
|
+
|
|
434
|
+
Picking a full contact without `property` still needs `READ_CONTACTS` below
|
|
435
|
+
Android 17, because the picker URI exposes no phone, email, or structured
|
|
436
|
+
name. On Android 17 and later the system contact picker returns a session
|
|
437
|
+
URI the plugin reads without that permission.
|
|
438
|
+
|
|
381
439
|
| Param | Type | Description |
|
|
382
440
|
| ------------- | ------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
383
441
|
| **`options`** | <code><a href="#pickcontactsoptions">PickContactsOptions</a></code> | - Optional fields to retrieve and picker configuration |
|
|
@@ -489,26 +547,27 @@ Options for creating a contact.
|
|
|
489
547
|
|
|
490
548
|
<a href="#contact">Contact</a> information.
|
|
491
549
|
|
|
492
|
-
| Prop | Type | Description
|
|
493
|
-
| ---------------------- | --------------------------------------------- |
|
|
494
|
-
| **`id`** | <code>string</code> | Unique identifier for the contact.
|
|
495
|
-
| **`
|
|
496
|
-
| **`
|
|
497
|
-
| **`
|
|
498
|
-
| **`
|
|
499
|
-
| **`
|
|
500
|
-
| **`
|
|
501
|
-
| **`
|
|
502
|
-
| **`
|
|
503
|
-
| **`
|
|
504
|
-
| **`
|
|
505
|
-
| **`
|
|
506
|
-
| **`
|
|
507
|
-
| **`
|
|
508
|
-
| **`
|
|
509
|
-
| **`
|
|
510
|
-
| **`
|
|
511
|
-
| **`
|
|
550
|
+
| Prop | Type | Description | Since |
|
|
551
|
+
| ---------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
|
|
552
|
+
| **`id`** | <code>string</code> | Unique identifier for the contact. | 1.0.0 |
|
|
553
|
+
| **`displayName`** | <code>string</code> | Formatted name shown by the device for this contact. Derived from `CNContactFormatter` on iOS and `DISPLAY_NAME` on Android. Read-only: setting this when creating or updating a contact has no effect. Use `givenName` and `familyName` to write a name. | 8.1.0 |
|
|
554
|
+
| **`account`** | <code><a href="#account">Account</a></code> | <a href="#account">Account</a> information for the contact. | 1.0.0 |
|
|
555
|
+
| **`birthday`** | <code><a href="#birthday">Birthday</a></code> | <a href="#birthday">Birthday</a> information for the contact. | 1.0.0 |
|
|
556
|
+
| **`emailAddresses`** | <code>EmailAddress[]</code> | Email addresses for the contact. | 1.0.0 |
|
|
557
|
+
| **`familyName`** | <code>string</code> | Family name (last name) of the contact. | 1.0.0 |
|
|
558
|
+
| **`fullName`** | <code>string</code> | Full name of the contact. | 1.0.0 |
|
|
559
|
+
| **`givenName`** | <code>string</code> | Given name (first name) of the contact. | 1.0.0 |
|
|
560
|
+
| **`groupIds`** | <code>string[]</code> | <a href="#group">Group</a> IDs the contact belongs to. | 1.0.0 |
|
|
561
|
+
| **`jobTitle`** | <code>string</code> | Job title of the contact. | 1.0.0 |
|
|
562
|
+
| **`middleName`** | <code>string</code> | Middle name of the contact. | 1.0.0 |
|
|
563
|
+
| **`namePrefix`** | <code>string</code> | Name prefix (e.g., "Dr.", "Mr.", "Ms.") of the contact. | 1.0.0 |
|
|
564
|
+
| **`nameSuffix`** | <code>string</code> | Name suffix (e.g., "Jr.", "Sr.", "III") of the contact. | 1.0.0 |
|
|
565
|
+
| **`note`** | <code>string</code> | Notes about the contact. | 1.0.0 |
|
|
566
|
+
| **`organizationName`** | <code>string</code> | Organization name of the contact. | 1.0.0 |
|
|
567
|
+
| **`phoneNumbers`** | <code>PhoneNumber[]</code> | Phone numbers for the contact. | 1.0.0 |
|
|
568
|
+
| **`photo`** | <code>string</code> | Base64-encoded photo of the contact. | 1.0.0 |
|
|
569
|
+
| **`postalAddresses`** | <code>PostalAddress[]</code> | Postal addresses for the contact. | 1.0.0 |
|
|
570
|
+
| **`urlAddresses`** | <code>UrlAddress[]</code> | URL addresses for the contact. | 1.0.0 |
|
|
512
571
|
|
|
513
572
|
|
|
514
573
|
#### Account
|
|
@@ -774,10 +833,11 @@ Result from picking contacts.
|
|
|
774
833
|
|
|
775
834
|
Options for picking contacts using the native contact picker.
|
|
776
835
|
|
|
777
|
-
| Prop | Type
|
|
778
|
-
| -------------- |
|
|
779
|
-
| **`fields`** | <code>(keyof <a href="#contact">Contact</a>)[]</code>
|
|
780
|
-
| **`multiple`** | <code>boolean</code>
|
|
836
|
+
| Prop | Type | Description | Since |
|
|
837
|
+
| -------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
|
|
838
|
+
| **`fields`** | <code>(keyof <a href="#contact">Contact</a>)[]</code> | Optional list of specific fields to retrieve. If not specified, all fields are returned. Ignored when {@link <a href="#pickcontactsoptions">PickContactsOptions.property</a>} is set. The result then contains only `id`, `displayName`, and the selected property. | 1.0.0 |
|
|
839
|
+
| **`multiple`** | <code>boolean</code> | Whether to allow selecting multiple contacts. Default is false. Ignored when {@link <a href="#pickcontactsoptions">PickContactsOptions.property</a>} is set. Property picking always returns a single value. | 1.0.0 |
|
|
840
|
+
| **`property`** | <code><a href="#contactproperty">ContactProperty</a></code> | Restrict the picker to a single contact property. On Android this launches the picker against the phone, email, or postal address table and reads the granted data row. That requires no `READ_CONTACTS` permission on any Android version, which is required for apps that only pick a contact detail and target Android 17 (API 37) or later under the Google Play Contacts Permissions policy. On iOS the picker lets the user choose one phone number, email address, or postal address. It never requires a contacts permission. | 8.1.0 |
|
|
781
841
|
|
|
782
842
|
|
|
783
843
|
#### UpdateContactByIdOptions
|
|
@@ -900,6 +960,18 @@ Type of contacts permission to request.
|
|
|
900
960
|
|
|
901
961
|
<code>'readContacts' | 'writeContacts'</code>
|
|
902
962
|
|
|
963
|
+
|
|
964
|
+
### Enums
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
#### ContactProperty
|
|
968
|
+
|
|
969
|
+
| Members | Value | Description | Since |
|
|
970
|
+
| ------------------- | ---------------------------- | ------------------------ | ----- |
|
|
971
|
+
| **`PhoneNumber`** | <code>'phoneNumber'</code> | A single phone number. | 8.1.0 |
|
|
972
|
+
| **`EmailAddress`** | <code>'emailAddress'</code> | A single email address. | 8.1.0 |
|
|
973
|
+
| **`PostalAddress`** | <code>'postalAddress'</code> | A single postal address. | 8.1.0 |
|
|
974
|
+
|
|
903
975
|
</docgen-api>
|
|
904
976
|
|
|
905
977
|
### Credit
|
|
@@ -40,7 +40,7 @@ import java.util.Set;
|
|
|
40
40
|
)
|
|
41
41
|
public class CapacitorContactsPlugin extends Plugin {
|
|
42
42
|
|
|
43
|
-
private final String pluginVersion = "8.0
|
|
43
|
+
private final String pluginVersion = "8.1.0";
|
|
44
44
|
private static final int BATCH_SIZE = 50;
|
|
45
45
|
|
|
46
46
|
// MARK: - Implemented API surface
|
|
@@ -467,19 +467,87 @@ public class CapacitorContactsPlugin extends Plugin {
|
|
|
467
467
|
private static final int EDIT_CONTACT_REQUEST = 7004;
|
|
468
468
|
|
|
469
469
|
private PluginCall currentPickerCall;
|
|
470
|
+
private String currentPickerProperty;
|
|
471
|
+
|
|
472
|
+
// Android 17 (API 37) system contact picker. Constants are inlined so the
|
|
473
|
+
// plugin still compiles against compileSdk 36.
|
|
474
|
+
private static final int ANDROID_API_17 = 37;
|
|
475
|
+
private static final String ACTION_PICK_CONTACTS = "android.provider.action.PICK_CONTACTS";
|
|
476
|
+
private static final String EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS = "android.provider.extra.PICK_CONTACTS_REQUESTED_DATA_FIELDS";
|
|
477
|
+
private static final String EXTRA_PICK_CONTACTS_SELECTION_LIMIT = "android.provider.extra.PICK_CONTACTS_SELECTION_LIMIT";
|
|
470
478
|
|
|
471
479
|
@PluginMethod
|
|
472
480
|
public void pickContact(PluginCall call) {
|
|
473
|
-
|
|
474
|
-
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
|
|
475
|
-
startActivityForResult(call, intent, PICK_CONTACT_REQUEST);
|
|
481
|
+
launchContactPicker(call);
|
|
476
482
|
}
|
|
477
483
|
|
|
478
484
|
@PluginMethod
|
|
479
485
|
public void pickContacts(PluginCall call) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
486
|
+
launchContactPicker(call);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private void launchContactPicker(PluginCall call) {
|
|
490
|
+
String property = call.getString("property");
|
|
491
|
+
if (property != null && contentUriForProperty(property) == null) {
|
|
492
|
+
call.reject("Invalid property. Use phoneNumber, emailAddress, or postalAddress.");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
currentPickerCall = call;
|
|
497
|
+
currentPickerProperty = property;
|
|
498
|
+
startActivityForResult(call, createPickIntent(call, property), PICK_CONTACT_REQUEST);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private Intent createPickIntent(PluginCall call, String property) {
|
|
502
|
+
if (property != null) {
|
|
503
|
+
return new Intent(Intent.ACTION_PICK, contentUriForProperty(property));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (android.os.Build.VERSION.SDK_INT >= ANDROID_API_17) {
|
|
507
|
+
Intent intent = new Intent(ACTION_PICK_CONTACTS);
|
|
508
|
+
intent.putStringArrayListExtra(EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS, requestedMimeTypes(call));
|
|
509
|
+
boolean multiple = Boolean.TRUE.equals(call.getBoolean("multiple", false));
|
|
510
|
+
if (!multiple) {
|
|
511
|
+
intent.putExtra(EXTRA_PICK_CONTACTS_SELECTION_LIMIT, 1);
|
|
512
|
+
} else {
|
|
513
|
+
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
|
514
|
+
}
|
|
515
|
+
if (intent.resolveActivity(getContext().getPackageManager()) != null) {
|
|
516
|
+
return intent;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private Uri contentUriForProperty(String property) {
|
|
524
|
+
if ("phoneNumber".equals(property)) {
|
|
525
|
+
return ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
|
|
526
|
+
}
|
|
527
|
+
if ("emailAddress".equals(property)) {
|
|
528
|
+
return ContactsContract.CommonDataKinds.Email.CONTENT_URI;
|
|
529
|
+
}
|
|
530
|
+
if ("postalAddress".equals(property)) {
|
|
531
|
+
return ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URI;
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private ArrayList<String> requestedMimeTypes(PluginCall call) {
|
|
537
|
+
Set<String> fields = parseFieldsArray(call);
|
|
538
|
+
if (fields != null) {
|
|
539
|
+
return new ArrayList<>(getMimeTypesForFields(fields));
|
|
540
|
+
}
|
|
541
|
+
ArrayList<String> mimeTypes = new ArrayList<>();
|
|
542
|
+
mimeTypes.add(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
|
|
543
|
+
mimeTypes.add(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
|
|
544
|
+
mimeTypes.add(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE);
|
|
545
|
+
mimeTypes.add(ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE);
|
|
546
|
+
mimeTypes.add(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE);
|
|
547
|
+
mimeTypes.add(ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE);
|
|
548
|
+
mimeTypes.add(ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE);
|
|
549
|
+
mimeTypes.add(ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE);
|
|
550
|
+
return mimeTypes;
|
|
483
551
|
}
|
|
484
552
|
|
|
485
553
|
@PluginMethod
|
|
@@ -546,6 +614,7 @@ public class CapacitorContactsPlugin extends Plugin {
|
|
|
546
614
|
currentPickerCall = null;
|
|
547
615
|
|
|
548
616
|
if (resultCode != android.app.Activity.RESULT_OK) {
|
|
617
|
+
currentPickerProperty = null;
|
|
549
618
|
if (requestCode == PICK_CONTACT_REQUEST) {
|
|
550
619
|
call.resolve(new JSObject().put("contacts", new JSArray()));
|
|
551
620
|
} else if (requestCode == CREATE_CONTACT_REQUEST) {
|
|
@@ -557,20 +626,9 @@ public class CapacitorContactsPlugin extends Plugin {
|
|
|
557
626
|
try {
|
|
558
627
|
if (requestCode == PICK_CONTACT_REQUEST) {
|
|
559
628
|
if (data != null && data.getData() != null) {
|
|
560
|
-
|
|
561
|
-
if (contactId != null) {
|
|
562
|
-
ContactBuilder builder = fetchContact(contactId, null);
|
|
563
|
-
if (builder != null) {
|
|
564
|
-
JSArray contacts = new JSArray();
|
|
565
|
-
contacts.put(builder.toJSObject(null));
|
|
566
|
-
call.resolve(new JSObject().put("contacts", contacts));
|
|
567
|
-
} else {
|
|
568
|
-
call.resolve(new JSObject().put("contacts", new JSArray()));
|
|
569
|
-
}
|
|
570
|
-
} else {
|
|
571
|
-
call.resolve(new JSObject().put("contacts", new JSArray()));
|
|
572
|
-
}
|
|
629
|
+
resolvePickedUri(call, data.getData());
|
|
573
630
|
} else {
|
|
631
|
+
currentPickerProperty = null;
|
|
574
632
|
call.resolve(new JSObject().put("contacts", new JSArray()));
|
|
575
633
|
}
|
|
576
634
|
} else if (requestCode == CREATE_CONTACT_REQUEST) {
|
|
@@ -590,6 +648,168 @@ public class CapacitorContactsPlugin extends Plugin {
|
|
|
590
648
|
}
|
|
591
649
|
}
|
|
592
650
|
|
|
651
|
+
private void resolvePickedUri(PluginCall call, Uri uri) {
|
|
652
|
+
String property = currentPickerProperty;
|
|
653
|
+
currentPickerProperty = null;
|
|
654
|
+
|
|
655
|
+
if (isSessionUri(uri)) {
|
|
656
|
+
call.resolve(new JSObject().put("contacts", contactsFromSessionUri(uri, fieldsForPickedContact(call))));
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (property != null || isDataRowUri(uri)) {
|
|
661
|
+
JSObject contact = contactFromDataRowUri(uri, property);
|
|
662
|
+
JSArray contacts = new JSArray();
|
|
663
|
+
if (contact != null) {
|
|
664
|
+
contacts.put(contact);
|
|
665
|
+
}
|
|
666
|
+
call.resolve(new JSObject().put("contacts", contacts));
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
call.resolve(new JSObject().put("contacts", contactsFromContactUri(uri, fieldsForPickedContact(call))));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
private Set<String> fieldsForPickedContact(PluginCall call) {
|
|
674
|
+
Set<String> fields = parseFieldsArray(call);
|
|
675
|
+
if (fields == null) {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
Set<String> withIdentity = new HashSet<>(fields);
|
|
679
|
+
withIdentity.add("id");
|
|
680
|
+
withIdentity.add("displayName");
|
|
681
|
+
return withIdentity;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
private boolean isSessionUri(Uri uri) {
|
|
685
|
+
String authority = uri.getAuthority();
|
|
686
|
+
return authority != null && authority.contains("picker");
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
private boolean isDataRowUri(Uri uri) {
|
|
690
|
+
List<String> segments = uri.getPathSegments();
|
|
691
|
+
return segments != null && !segments.isEmpty() && "data".equals(segments.get(0));
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private JSArray contactsFromSessionUri(Uri sessionUri, Set<String> fields) {
|
|
695
|
+
JSArray contacts = new JSArray();
|
|
696
|
+
Map<String, ContactBuilder> builders = new java.util.LinkedHashMap<>();
|
|
697
|
+
ContentResolver resolver = getContext().getContentResolver();
|
|
698
|
+
|
|
699
|
+
try (Cursor cursor = resolver.query(sessionUri, null, null, null, null)) {
|
|
700
|
+
if (cursor == null) {
|
|
701
|
+
return contacts;
|
|
702
|
+
}
|
|
703
|
+
while (cursor.moveToNext()) {
|
|
704
|
+
String contactId = getStringColumn(cursor, ContactsContract.Data.CONTACT_ID);
|
|
705
|
+
if (contactId == null) {
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
ContactBuilder builder = builders.get(contactId);
|
|
709
|
+
if (builder == null) {
|
|
710
|
+
builder = new ContactBuilder(contactId);
|
|
711
|
+
String displayName = getStringColumn(cursor, ContactsContract.Data.DISPLAY_NAME);
|
|
712
|
+
builder.displayName = displayName;
|
|
713
|
+
builder.fullName = displayName;
|
|
714
|
+
builders.put(contactId, builder);
|
|
715
|
+
}
|
|
716
|
+
processDataRow(builder, cursor);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
for (ContactBuilder builder : builders.values()) {
|
|
721
|
+
contacts.put(builder.toJSObject(fields));
|
|
722
|
+
}
|
|
723
|
+
return contacts;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private JSObject contactFromDataRowUri(Uri uri, String property) {
|
|
727
|
+
ContentResolver resolver = getContext().getContentResolver();
|
|
728
|
+
try (Cursor cursor = resolver.query(uri, null, null, null, null)) {
|
|
729
|
+
if (cursor == null || !cursor.moveToFirst()) {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
String contactId = getStringColumn(cursor, ContactsContract.Data.CONTACT_ID);
|
|
734
|
+
ContactBuilder builder = new ContactBuilder(contactId != null ? contactId : uri.getLastPathSegment());
|
|
735
|
+
String displayName = getStringColumn(cursor, ContactsContract.Data.DISPLAY_NAME);
|
|
736
|
+
builder.displayName = displayName;
|
|
737
|
+
builder.fullName = displayName;
|
|
738
|
+
processDataRow(builder, cursor);
|
|
739
|
+
|
|
740
|
+
Set<String> fields = new HashSet<>();
|
|
741
|
+
fields.add("id");
|
|
742
|
+
fields.add("displayName");
|
|
743
|
+
if ("emailAddress".equals(property)) {
|
|
744
|
+
fields.add("emailAddresses");
|
|
745
|
+
} else if ("postalAddress".equals(property)) {
|
|
746
|
+
fields.add("postalAddresses");
|
|
747
|
+
} else {
|
|
748
|
+
fields.add("phoneNumbers");
|
|
749
|
+
}
|
|
750
|
+
return builder.toJSObject(fields);
|
|
751
|
+
} catch (Exception ex) {
|
|
752
|
+
android.util.Log.w("CapacitorContacts", "Failed to read picked contact data row", ex);
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
private JSArray contactsFromContactUri(Uri contactUri, Set<String> fields) {
|
|
758
|
+
JSArray contacts = new JSArray();
|
|
759
|
+
String contactId = getContactIdFromUri(contactUri);
|
|
760
|
+
if (contactId == null) {
|
|
761
|
+
return contacts;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (hasReadPermission()) {
|
|
765
|
+
try {
|
|
766
|
+
ContactBuilder builder = fetchContact(contactId, fields);
|
|
767
|
+
if (builder != null) {
|
|
768
|
+
if (builder.displayName == null) {
|
|
769
|
+
builder.displayName = builder.fullName;
|
|
770
|
+
}
|
|
771
|
+
contacts.put(builder.toJSObject(fields));
|
|
772
|
+
}
|
|
773
|
+
return contacts;
|
|
774
|
+
} catch (Exception ex) {
|
|
775
|
+
android.util.Log.w("CapacitorContacts", "Failed to fetch picked contact", ex);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
ContactBuilder builder = new ContactBuilder(contactId);
|
|
780
|
+
String displayName = resolveDisplayNameFromUri(contactUri);
|
|
781
|
+
builder.displayName = displayName;
|
|
782
|
+
builder.fullName = displayName;
|
|
783
|
+
Set<String> thinFields = new HashSet<>();
|
|
784
|
+
thinFields.add("id");
|
|
785
|
+
thinFields.add("displayName");
|
|
786
|
+
thinFields.add("fullName");
|
|
787
|
+
contacts.put(builder.toJSObject(thinFields));
|
|
788
|
+
return contacts;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
private String resolveDisplayNameFromUri(Uri contactUri) {
|
|
792
|
+
ContentResolver resolver = getContext().getContentResolver();
|
|
793
|
+
try (
|
|
794
|
+
Cursor cursor = resolver.query(contactUri, new String[] { ContactsContract.Contacts.DISPLAY_NAME_PRIMARY }, null, null, null)
|
|
795
|
+
) {
|
|
796
|
+
if (cursor != null && cursor.moveToFirst()) {
|
|
797
|
+
return cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY));
|
|
798
|
+
}
|
|
799
|
+
} catch (Exception ex) {
|
|
800
|
+
// The picker URI grant may only expose a subset of columns.
|
|
801
|
+
}
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
private static String getStringColumn(Cursor cursor, String column) {
|
|
806
|
+
int index = cursor.getColumnIndex(column);
|
|
807
|
+
if (index < 0) {
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
return cursor.getString(index);
|
|
811
|
+
}
|
|
812
|
+
|
|
593
813
|
private String getContactIdFromUri(Uri contactUri) {
|
|
594
814
|
ContentResolver resolver = getContext().getContentResolver();
|
|
595
815
|
try (Cursor cursor = resolver.query(contactUri, new String[] { ContactsContract.Contacts._ID }, null, null, null)) {
|
|
@@ -1110,6 +1330,7 @@ public class CapacitorContactsPlugin extends Plugin {
|
|
|
1110
1330
|
String id = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID));
|
|
1111
1331
|
String displayName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY));
|
|
1112
1332
|
ContactBuilder builder = new ContactBuilder(id);
|
|
1333
|
+
builder.displayName = displayName;
|
|
1113
1334
|
builder.fullName = displayName;
|
|
1114
1335
|
builderMap.put(id, builder);
|
|
1115
1336
|
}
|
|
@@ -1292,6 +1513,9 @@ public class CapacitorContactsPlugin extends Plugin {
|
|
|
1292
1513
|
if (builder.fullName == null) {
|
|
1293
1514
|
builder.fullName = resolveDisplayName(contactId);
|
|
1294
1515
|
}
|
|
1516
|
+
if (builder.displayName == null) {
|
|
1517
|
+
builder.displayName = builder.fullName;
|
|
1518
|
+
}
|
|
1295
1519
|
|
|
1296
1520
|
return builder;
|
|
1297
1521
|
}
|
|
@@ -1480,6 +1704,7 @@ public class CapacitorContactsPlugin extends Plugin {
|
|
|
1480
1704
|
String jobTitle;
|
|
1481
1705
|
String note;
|
|
1482
1706
|
String fullName;
|
|
1707
|
+
String displayName;
|
|
1483
1708
|
String photoBase64;
|
|
1484
1709
|
String accountName;
|
|
1485
1710
|
String accountType;
|
|
@@ -1612,6 +1837,7 @@ public class CapacitorContactsPlugin extends Plugin {
|
|
|
1612
1837
|
if (includeAll || fields.contains("jobTitle")) contact.put("jobTitle", jobTitle);
|
|
1613
1838
|
if (includeAll || fields.contains("note")) contact.put("note", note);
|
|
1614
1839
|
if (includeAll || fields.contains("fullName")) contact.put("fullName", fullName);
|
|
1840
|
+
if (includeAll || fields.contains("displayName")) contact.put("displayName", displayName != null ? displayName : fullName);
|
|
1615
1841
|
if (includeAll || fields.contains("photo")) contact.put("photo", photoBase64);
|
|
1616
1842
|
if (includeAll || fields.contains("groupIds")) contact.put("groupIds", groupIds);
|
|
1617
1843
|
if (includeAll || fields.contains("emailAddresses")) contact.put("emailAddresses", emailAddresses);
|
package/dist/docs.json
CHANGED
|
@@ -475,7 +475,7 @@
|
|
|
475
475
|
"text": "1.0.0"
|
|
476
476
|
}
|
|
477
477
|
],
|
|
478
|
-
"docs": "Pick a single contact using the native contact picker.",
|
|
478
|
+
"docs": "Pick a single contact using the native contact picker.\n\nSame options as {@link pickContacts}. Pass `property` to select one phone\nnumber, email address, or postal address without `READ_CONTACTS` on Android.",
|
|
479
479
|
"complexTypes": [
|
|
480
480
|
"PickContactsResult",
|
|
481
481
|
"PickContactOptions",
|
|
@@ -508,7 +508,7 @@
|
|
|
508
508
|
"text": "1.0.0"
|
|
509
509
|
}
|
|
510
510
|
],
|
|
511
|
-
"docs": "Pick one or more contacts using the native contact picker.",
|
|
511
|
+
"docs": "Pick one or more contacts using the native contact picker.\n\nOn iOS this never requires a contacts permission.\n\nOn Android, pass `property` to select a single phone number, email address,\nor postal address with no `READ_CONTACTS` permission. That works on every\nAndroid version and is the way to ship without declaring the permission\nwhen targeting Android 17 (API 37) or later.\n\nPicking a full contact without `property` still needs `READ_CONTACTS` below\nAndroid 17, because the picker URI exposes no phone, email, or structured\nname. On Android 17 and later the system contact picker returns a session\nURI the plugin reads without that permission.",
|
|
512
512
|
"complexTypes": [
|
|
513
513
|
"PickContactsResult",
|
|
514
514
|
"PickContactsOptions"
|
|
@@ -727,6 +727,18 @@
|
|
|
727
727
|
"complexTypes": [],
|
|
728
728
|
"type": "string | undefined"
|
|
729
729
|
},
|
|
730
|
+
{
|
|
731
|
+
"name": "displayName",
|
|
732
|
+
"tags": [
|
|
733
|
+
{
|
|
734
|
+
"text": "8.1.0",
|
|
735
|
+
"name": "since"
|
|
736
|
+
}
|
|
737
|
+
],
|
|
738
|
+
"docs": "Formatted name shown by the device for this contact.\n\nDerived from `CNContactFormatter` on iOS and `DISPLAY_NAME` on Android.\nRead-only: setting this when creating or updating a contact has no effect.\nUse `givenName` and `familyName` to write a name.",
|
|
739
|
+
"complexTypes": [],
|
|
740
|
+
"type": "string | undefined"
|
|
741
|
+
},
|
|
730
742
|
{
|
|
731
743
|
"name": "account",
|
|
732
744
|
"tags": [
|
|
@@ -1971,7 +1983,7 @@
|
|
|
1971
1983
|
"name": "since"
|
|
1972
1984
|
}
|
|
1973
1985
|
],
|
|
1974
|
-
"docs": "Optional list of specific fields to retrieve. If not specified, all fields are returned.",
|
|
1986
|
+
"docs": "Optional list of specific fields to retrieve. If not specified, all fields are returned.\n\nIgnored when {@link PickContactsOptions.property} is set. The result then\ncontains only `id`, `displayName`, and the selected property.",
|
|
1975
1987
|
"complexTypes": [
|
|
1976
1988
|
"ContactField"
|
|
1977
1989
|
],
|
|
@@ -1985,9 +1997,23 @@
|
|
|
1985
1997
|
"name": "since"
|
|
1986
1998
|
}
|
|
1987
1999
|
],
|
|
1988
|
-
"docs": "Whether to allow selecting multiple contacts. Default is false.",
|
|
2000
|
+
"docs": "Whether to allow selecting multiple contacts. Default is false.\n\nIgnored when {@link PickContactsOptions.property} is set. Property picking\nalways returns a single value.",
|
|
1989
2001
|
"complexTypes": [],
|
|
1990
2002
|
"type": "boolean | undefined"
|
|
2003
|
+
},
|
|
2004
|
+
{
|
|
2005
|
+
"name": "property",
|
|
2006
|
+
"tags": [
|
|
2007
|
+
{
|
|
2008
|
+
"text": "8.1.0",
|
|
2009
|
+
"name": "since"
|
|
2010
|
+
}
|
|
2011
|
+
],
|
|
2012
|
+
"docs": "Restrict the picker to a single contact property.\n\nOn Android this launches the picker against the phone, email, or postal\naddress table and reads the granted data row. That requires no\n`READ_CONTACTS` permission on any Android version, which is required for\napps that only pick a contact detail and target Android 17 (API 37) or\nlater under the Google Play Contacts Permissions policy.\n\nOn iOS the picker lets the user choose one phone number, email address,\nor postal address. It never requires a contacts permission.",
|
|
2013
|
+
"complexTypes": [
|
|
2014
|
+
"ContactProperty"
|
|
2015
|
+
],
|
|
2016
|
+
"type": "ContactProperty"
|
|
1991
2017
|
}
|
|
1992
2018
|
]
|
|
1993
2019
|
},
|
|
@@ -2103,7 +2129,47 @@
|
|
|
2103
2129
|
]
|
|
2104
2130
|
}
|
|
2105
2131
|
],
|
|
2106
|
-
"enums": [
|
|
2132
|
+
"enums": [
|
|
2133
|
+
{
|
|
2134
|
+
"name": "ContactProperty",
|
|
2135
|
+
"slug": "contactproperty",
|
|
2136
|
+
"members": [
|
|
2137
|
+
{
|
|
2138
|
+
"name": "PhoneNumber",
|
|
2139
|
+
"value": "'phoneNumber'",
|
|
2140
|
+
"tags": [
|
|
2141
|
+
{
|
|
2142
|
+
"text": "8.1.0",
|
|
2143
|
+
"name": "since"
|
|
2144
|
+
}
|
|
2145
|
+
],
|
|
2146
|
+
"docs": "A single phone number."
|
|
2147
|
+
},
|
|
2148
|
+
{
|
|
2149
|
+
"name": "EmailAddress",
|
|
2150
|
+
"value": "'emailAddress'",
|
|
2151
|
+
"tags": [
|
|
2152
|
+
{
|
|
2153
|
+
"text": "8.1.0",
|
|
2154
|
+
"name": "since"
|
|
2155
|
+
}
|
|
2156
|
+
],
|
|
2157
|
+
"docs": "A single email address."
|
|
2158
|
+
},
|
|
2159
|
+
{
|
|
2160
|
+
"name": "PostalAddress",
|
|
2161
|
+
"value": "'postalAddress'",
|
|
2162
|
+
"tags": [
|
|
2163
|
+
{
|
|
2164
|
+
"text": "8.1.0",
|
|
2165
|
+
"name": "since"
|
|
2166
|
+
}
|
|
2167
|
+
],
|
|
2168
|
+
"docs": "A single postal address."
|
|
2169
|
+
}
|
|
2170
|
+
]
|
|
2171
|
+
}
|
|
2172
|
+
],
|
|
2107
2173
|
"typeAliases": [
|
|
2108
2174
|
{
|
|
2109
2175
|
"name": "Omit",
|