@capgo/capacitor-contacts 7.1.2 → 8.0.1

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.
@@ -39,6 +39,8 @@ import java.util.Set;
39
39
  )
40
40
  public class CapacitorContactsPlugin extends Plugin {
41
41
 
42
+ private final String pluginVersion = "8.0.1";
43
+
42
44
  // MARK: - Implemented API surface
43
45
 
44
46
  @PluginMethod
@@ -193,70 +195,835 @@ public class CapacitorContactsPlugin extends Plugin {
193
195
  call.resolve(status);
194
196
  }
195
197
 
196
- // MARK: - Not yet implemented operations
197
-
198
- private void notImplemented(PluginCall call) {
199
- call.reject("Method not implemented yet.");
198
+ @PluginMethod
199
+ public void getPluginVersion(PluginCall call) {
200
+ try {
201
+ JSObject ret = new JSObject();
202
+ ret.put("version", pluginVersion);
203
+ call.resolve(ret);
204
+ } catch (Exception e) {
205
+ call.reject("Could not get plugin version", e);
206
+ }
200
207
  }
201
208
 
209
+ // MARK: - Write operations
210
+
202
211
  @PluginMethod
203
212
  public void createContact(PluginCall call) {
204
- notImplemented(call);
213
+ if (!hasWritePermission()) {
214
+ call.reject("WRITE_CONTACTS permission not granted.");
215
+ return;
216
+ }
217
+
218
+ JSObject options = call.getObject("options", new JSObject());
219
+ JSObject contactData = options.getJSObject("contact");
220
+ if (contactData == null) {
221
+ call.reject("Missing contact data.");
222
+ return;
223
+ }
224
+
225
+ try {
226
+ String contactId = insertContact(contactData);
227
+ call.resolve(new JSObject().put("id", contactId));
228
+ } catch (Exception ex) {
229
+ call.reject("Failed to create contact.", null, ex);
230
+ }
205
231
  }
206
232
 
207
233
  @PluginMethod
208
- public void createGroup(PluginCall call) {
209
- notImplemented(call);
234
+ public void updateContactById(PluginCall call) {
235
+ if (!hasWritePermission()) {
236
+ call.reject("WRITE_CONTACTS permission not granted.");
237
+ return;
238
+ }
239
+
240
+ JSObject options = call.getObject("options", new JSObject());
241
+ String contactId = options.getString("id");
242
+ JSObject contactData = options.getJSObject("contact");
243
+
244
+ if (contactId == null || contactData == null) {
245
+ call.reject("Missing contact identifier or data.");
246
+ return;
247
+ }
248
+
249
+ try {
250
+ updateContact(contactId, contactData);
251
+ call.resolve();
252
+ } catch (Exception ex) {
253
+ call.reject("Failed to update contact.", null, ex);
254
+ }
210
255
  }
211
256
 
212
257
  @PluginMethod
213
258
  public void deleteContactById(PluginCall call) {
214
- notImplemented(call);
259
+ if (!hasWritePermission()) {
260
+ call.reject("WRITE_CONTACTS permission not granted.");
261
+ return;
262
+ }
263
+
264
+ JSObject options = call.getObject("options", new JSObject());
265
+ String contactId = options.getString("id");
266
+
267
+ if (contactId == null) {
268
+ call.reject("Missing contact identifier.");
269
+ return;
270
+ }
271
+
272
+ try {
273
+ ContentResolver resolver = getContext().getContentResolver();
274
+ Uri contactUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, Long.parseLong(contactId));
275
+ int rowsDeleted = resolver.delete(contactUri, null, null);
276
+ if (rowsDeleted > 0) {
277
+ call.resolve();
278
+ } else {
279
+ call.reject("Contact not found or could not be deleted.");
280
+ }
281
+ } catch (Exception ex) {
282
+ call.reject("Failed to delete contact.", null, ex);
283
+ }
215
284
  }
216
285
 
286
+ // MARK: - Group operations
287
+
217
288
  @PluginMethod
218
- public void deleteGroupById(PluginCall call) {
219
- notImplemented(call);
289
+ public void getGroups(PluginCall call) {
290
+ if (!hasReadPermission()) {
291
+ call.reject("READ_CONTACTS permission not granted.");
292
+ return;
293
+ }
294
+
295
+ ContentResolver resolver = getContext().getContentResolver();
296
+ JSArray groups = new JSArray();
297
+
298
+ try (
299
+ Cursor cursor = resolver.query(
300
+ ContactsContract.Groups.CONTENT_URI,
301
+ new String[] { ContactsContract.Groups._ID, ContactsContract.Groups.TITLE },
302
+ null,
303
+ null,
304
+ ContactsContract.Groups.TITLE + " ASC"
305
+ )
306
+ ) {
307
+ if (cursor != null) {
308
+ while (cursor.moveToNext()) {
309
+ String id = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Groups._ID));
310
+ String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Groups.TITLE));
311
+ JSObject group = new JSObject();
312
+ group.put("id", id);
313
+ group.put("name", name);
314
+ groups.put(group);
315
+ }
316
+ }
317
+ call.resolve(new JSObject().put("groups", groups));
318
+ } catch (Exception ex) {
319
+ call.reject("Failed to fetch groups.", null, ex);
320
+ }
220
321
  }
221
322
 
222
323
  @PluginMethod
223
- public void displayContactById(PluginCall call) {
224
- notImplemented(call);
324
+ public void getGroupById(PluginCall call) {
325
+ if (!hasReadPermission()) {
326
+ call.reject("READ_CONTACTS permission not granted.");
327
+ return;
328
+ }
329
+
330
+ JSObject options = call.getObject("options", new JSObject());
331
+ String groupId = options.getString("id");
332
+
333
+ if (groupId == null) {
334
+ call.reject("Missing group identifier.");
335
+ return;
336
+ }
337
+
338
+ ContentResolver resolver = getContext().getContentResolver();
339
+
340
+ try (
341
+ Cursor cursor = resolver.query(
342
+ ContactsContract.Groups.CONTENT_URI,
343
+ new String[] { ContactsContract.Groups._ID, ContactsContract.Groups.TITLE },
344
+ ContactsContract.Groups._ID + " = ?",
345
+ new String[] { groupId },
346
+ null
347
+ )
348
+ ) {
349
+ if (cursor != null && cursor.moveToFirst()) {
350
+ String id = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Groups._ID));
351
+ String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Groups.TITLE));
352
+ JSObject group = new JSObject();
353
+ group.put("id", id);
354
+ group.put("name", name);
355
+ call.resolve(new JSObject().put("group", group));
356
+ } else {
357
+ call.resolve(new JSObject().put("group", null));
358
+ }
359
+ } catch (Exception ex) {
360
+ call.reject("Failed to fetch group.", null, ex);
361
+ }
225
362
  }
226
363
 
227
364
  @PluginMethod
228
- public void displayCreateContact(PluginCall call) {
229
- notImplemented(call);
365
+ public void createGroup(PluginCall call) {
366
+ if (!hasWritePermission()) {
367
+ call.reject("WRITE_CONTACTS permission not granted.");
368
+ return;
369
+ }
370
+
371
+ JSObject options = call.getObject("options", new JSObject());
372
+ JSObject groupData = options.getJSObject("group");
373
+
374
+ if (groupData == null) {
375
+ call.reject("Missing group data.");
376
+ return;
377
+ }
378
+
379
+ String name = groupData.getString("name");
380
+ if (name == null) {
381
+ call.reject("Missing group name.");
382
+ return;
383
+ }
384
+
385
+ try {
386
+ ContentResolver resolver = getContext().getContentResolver();
387
+ ArrayList<android.content.ContentProviderOperation> ops = new ArrayList<>();
388
+
389
+ ops.add(
390
+ android.content.ContentProviderOperation.newInsert(ContactsContract.Groups.CONTENT_URI)
391
+ .withValue(ContactsContract.Groups.TITLE, name)
392
+ .build()
393
+ );
394
+
395
+ android.content.ContentProviderResult[] results = resolver.applyBatch(ContactsContract.AUTHORITY, ops);
396
+ if (results.length > 0 && results[0].uri != null) {
397
+ String groupId = results[0].uri.getLastPathSegment();
398
+ call.resolve(new JSObject().put("id", groupId));
399
+ } else {
400
+ call.reject("Failed to create group.");
401
+ }
402
+ } catch (Exception ex) {
403
+ call.reject("Failed to create group.", null, ex);
404
+ }
230
405
  }
231
406
 
232
407
  @PluginMethod
233
- public void displayUpdateContactById(PluginCall call) {
234
- notImplemented(call);
408
+ public void deleteGroupById(PluginCall call) {
409
+ if (!hasWritePermission()) {
410
+ call.reject("WRITE_CONTACTS permission not granted.");
411
+ return;
412
+ }
413
+
414
+ JSObject options = call.getObject("options", new JSObject());
415
+ String groupId = options.getString("id");
416
+
417
+ if (groupId == null) {
418
+ call.reject("Missing group identifier.");
419
+ return;
420
+ }
421
+
422
+ try {
423
+ ContentResolver resolver = getContext().getContentResolver();
424
+ int rowsDeleted = resolver.delete(
425
+ ContactsContract.Groups.CONTENT_URI,
426
+ ContactsContract.Groups._ID + " = ?",
427
+ new String[] { groupId }
428
+ );
429
+
430
+ if (rowsDeleted > 0) {
431
+ call.resolve();
432
+ } else {
433
+ call.reject("Group not found or could not be deleted.");
434
+ }
435
+ } catch (Exception ex) {
436
+ call.reject("Failed to delete group.", null, ex);
437
+ }
235
438
  }
236
439
 
440
+ // MARK: - UI picker and display operations
441
+
442
+ private static final int PICK_CONTACT_REQUEST = 7001;
443
+ private static final int VIEW_CONTACT_REQUEST = 7002;
444
+ private static final int CREATE_CONTACT_REQUEST = 7003;
445
+ private static final int EDIT_CONTACT_REQUEST = 7004;
446
+
447
+ private PluginCall currentPickerCall;
448
+
237
449
  @PluginMethod
238
- public void getGroupById(PluginCall call) {
239
- notImplemented(call);
450
+ public void pickContact(PluginCall call) {
451
+ currentPickerCall = call;
452
+ Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
453
+ startActivityForResult(call, intent, PICK_CONTACT_REQUEST);
240
454
  }
241
455
 
242
456
  @PluginMethod
243
- public void getGroups(PluginCall call) {
244
- notImplemented(call);
457
+ public void pickContacts(PluginCall call) {
458
+ // Android doesn't have a native multi-select contact picker
459
+ // Fall back to single selection
460
+ pickContact(call);
245
461
  }
246
462
 
247
463
  @PluginMethod
248
- public void pickContact(PluginCall call) {
249
- notImplemented(call);
464
+ public void displayContactById(PluginCall call) {
465
+ JSObject options = call.getObject("options", new JSObject());
466
+ String contactId = options.getString("id");
467
+
468
+ if (contactId == null) {
469
+ call.reject("Missing contact identifier.");
470
+ return;
471
+ }
472
+
473
+ try {
474
+ Uri contactUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, Long.parseLong(contactId));
475
+ Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
476
+ getContext().startActivity(intent);
477
+ call.resolve();
478
+ } catch (Exception ex) {
479
+ call.reject("Failed to display contact.", null, ex);
480
+ }
250
481
  }
251
482
 
252
483
  @PluginMethod
253
- public void pickContacts(PluginCall call) {
254
- notImplemented(call);
484
+ public void displayCreateContact(PluginCall call) {
485
+ currentPickerCall = call;
486
+ Intent intent = new Intent(Intent.ACTION_INSERT, ContactsContract.Contacts.CONTENT_URI);
487
+
488
+ JSObject options = call.getObject("options", new JSObject());
489
+ if (options != null) {
490
+ JSObject contactData = options.getJSObject("contact");
491
+ if (contactData != null) {
492
+ populateIntent(intent, contactData);
493
+ }
494
+ }
495
+
496
+ startActivityForResult(call, intent, CREATE_CONTACT_REQUEST);
255
497
  }
256
498
 
257
499
  @PluginMethod
258
- public void updateContactById(PluginCall call) {
259
- notImplemented(call);
500
+ public void displayUpdateContactById(PluginCall call) {
501
+ JSObject options = call.getObject("options", new JSObject());
502
+ String contactId = options.getString("id");
503
+
504
+ if (contactId == null) {
505
+ call.reject("Missing contact identifier.");
506
+ return;
507
+ }
508
+
509
+ try {
510
+ Uri contactUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, Long.parseLong(contactId));
511
+ Intent intent = new Intent(Intent.ACTION_EDIT, contactUri);
512
+ intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
513
+ getContext().startActivity(intent);
514
+ call.resolve();
515
+ } catch (Exception ex) {
516
+ call.reject("Failed to display contact for editing.", null, ex);
517
+ }
518
+ }
519
+
520
+ @Override
521
+ protected void handleOnActivityResult(int requestCode, int resultCode, Intent data) {
522
+ super.handleOnActivityResult(requestCode, resultCode, data);
523
+
524
+ if (currentPickerCall == null) {
525
+ return;
526
+ }
527
+
528
+ PluginCall call = currentPickerCall;
529
+ currentPickerCall = null;
530
+
531
+ if (resultCode != android.app.Activity.RESULT_OK) {
532
+ if (requestCode == PICK_CONTACT_REQUEST) {
533
+ call.resolve(new JSObject().put("contacts", new JSArray()));
534
+ } else if (requestCode == CREATE_CONTACT_REQUEST) {
535
+ call.resolve(new JSObject());
536
+ }
537
+ return;
538
+ }
539
+
540
+ try {
541
+ if (requestCode == PICK_CONTACT_REQUEST) {
542
+ if (data != null && data.getData() != null) {
543
+ String contactId = getContactIdFromUri(data.getData());
544
+ if (contactId != null) {
545
+ ContactBuilder builder = fetchContact(contactId);
546
+ if (builder != null) {
547
+ JSArray contacts = new JSArray();
548
+ contacts.put(builder.toJSObject());
549
+ call.resolve(new JSObject().put("contacts", contacts));
550
+ } else {
551
+ call.resolve(new JSObject().put("contacts", new JSArray()));
552
+ }
553
+ } else {
554
+ call.resolve(new JSObject().put("contacts", new JSArray()));
555
+ }
556
+ } else {
557
+ call.resolve(new JSObject().put("contacts", new JSArray()));
558
+ }
559
+ } else if (requestCode == CREATE_CONTACT_REQUEST) {
560
+ if (data != null && data.getData() != null) {
561
+ String contactId = getContactIdFromUri(data.getData());
562
+ if (contactId != null) {
563
+ call.resolve(new JSObject().put("id", contactId));
564
+ } else {
565
+ call.resolve(new JSObject());
566
+ }
567
+ } else {
568
+ call.resolve(new JSObject());
569
+ }
570
+ }
571
+ } catch (Exception ex) {
572
+ call.reject("Failed to process contact picker result.", null, ex);
573
+ }
574
+ }
575
+
576
+ private String getContactIdFromUri(Uri contactUri) {
577
+ ContentResolver resolver = getContext().getContentResolver();
578
+ try (Cursor cursor = resolver.query(contactUri, new String[] { ContactsContract.Contacts._ID }, null, null, null)) {
579
+ if (cursor != null && cursor.moveToFirst()) {
580
+ return cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID));
581
+ }
582
+ } catch (Exception ex) {
583
+ // Ignore
584
+ }
585
+ return null;
586
+ }
587
+
588
+ // MARK: - Contact write helpers
589
+
590
+ private String insertContact(JSObject contactData) throws Exception {
591
+ ContentResolver resolver = getContext().getContentResolver();
592
+ ArrayList<android.content.ContentProviderOperation> ops = new ArrayList<>();
593
+
594
+ int rawContactInsertIndex = ops.size();
595
+ ops.add(
596
+ android.content.ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
597
+ .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, (String) null)
598
+ .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, (String) null)
599
+ .build()
600
+ );
601
+
602
+ // Add structured name
603
+ if (
604
+ contactData.has("givenName") ||
605
+ contactData.has("familyName") ||
606
+ contactData.has("middleName") ||
607
+ contactData.has("namePrefix") ||
608
+ contactData.has("nameSuffix")
609
+ ) {
610
+ ops.add(
611
+ android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
612
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex)
613
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
614
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, contactData.getString("givenName"))
615
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, contactData.getString("familyName"))
616
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, contactData.getString("middleName"))
617
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.PREFIX, contactData.getString("namePrefix"))
618
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, contactData.getString("nameSuffix"))
619
+ .build()
620
+ );
621
+ }
622
+
623
+ // Add organization
624
+ if (contactData.has("organizationName") || contactData.has("jobTitle")) {
625
+ ops.add(
626
+ android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
627
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex)
628
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE)
629
+ .withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, contactData.getString("organizationName"))
630
+ .withValue(ContactsContract.CommonDataKinds.Organization.TITLE, contactData.getString("jobTitle"))
631
+ .build()
632
+ );
633
+ }
634
+
635
+ // Add note
636
+ if (contactData.has("note")) {
637
+ ops.add(
638
+ android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
639
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex)
640
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE)
641
+ .withValue(ContactsContract.CommonDataKinds.Note.NOTE, contactData.getString("note"))
642
+ .build()
643
+ );
644
+ }
645
+
646
+ // Add email addresses
647
+ if (contactData.has("emailAddresses")) {
648
+ try {
649
+ org.json.JSONArray emails = contactData.getJSONArray("emailAddresses");
650
+ for (int i = 0; i < emails.length(); i++) {
651
+ org.json.JSONObject email = emails.getJSONObject(i);
652
+ String value = email.optString("value");
653
+ String type = email.optString("type", "OTHER");
654
+ String label = email.optString("label");
655
+
656
+ ops.add(
657
+ android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
658
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex)
659
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
660
+ .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, value)
661
+ .withValue(ContactsContract.CommonDataKinds.Email.TYPE, reverseMapEmailType(type))
662
+ .withValue(ContactsContract.CommonDataKinds.Email.LABEL, label)
663
+ .build()
664
+ );
665
+ }
666
+ } catch (Exception ex) {
667
+ // Ignore
668
+ }
669
+ }
670
+
671
+ // Add phone numbers
672
+ if (contactData.has("phoneNumbers")) {
673
+ try {
674
+ org.json.JSONArray phones = contactData.getJSONArray("phoneNumbers");
675
+ for (int i = 0; i < phones.length(); i++) {
676
+ org.json.JSONObject phone = phones.getJSONObject(i);
677
+ String value = phone.optString("value");
678
+ String type = phone.optString("type", "OTHER");
679
+ String label = phone.optString("label");
680
+
681
+ ops.add(
682
+ android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
683
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex)
684
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
685
+ .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, value)
686
+ .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, reverseMapPhoneType(type))
687
+ .withValue(ContactsContract.CommonDataKinds.Phone.LABEL, label)
688
+ .build()
689
+ );
690
+ }
691
+ } catch (Exception ex) {
692
+ // Ignore
693
+ }
694
+ }
695
+
696
+ // Add postal addresses
697
+ if (contactData.has("postalAddresses")) {
698
+ try {
699
+ org.json.JSONArray addresses = contactData.getJSONArray("postalAddresses");
700
+ for (int i = 0; i < addresses.length(); i++) {
701
+ org.json.JSONObject address = addresses.getJSONObject(i);
702
+
703
+ ops.add(
704
+ android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
705
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex)
706
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)
707
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.STREET, address.optString("street"))
708
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.CITY, address.optString("city"))
709
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.REGION, address.optString("state"))
710
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, address.optString("postalCode"))
711
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY, address.optString("country"))
712
+ .withValue(
713
+ ContactsContract.CommonDataKinds.StructuredPostal.TYPE,
714
+ reverseMapPostalType(address.optString("type", "OTHER"))
715
+ )
716
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.LABEL, address.optString("label"))
717
+ .build()
718
+ );
719
+ }
720
+ } catch (Exception ex) {
721
+ // Ignore
722
+ }
723
+ }
724
+
725
+ // Add URL addresses
726
+ if (contactData.has("urlAddresses")) {
727
+ try {
728
+ org.json.JSONArray urls = contactData.getJSONArray("urlAddresses");
729
+ for (int i = 0; i < urls.length(); i++) {
730
+ org.json.JSONObject url = urls.getJSONObject(i);
731
+
732
+ ops.add(
733
+ android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
734
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex)
735
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE)
736
+ .withValue(ContactsContract.CommonDataKinds.Website.URL, url.optString("value"))
737
+ .withValue(ContactsContract.CommonDataKinds.Website.TYPE, reverseMapUrlType(url.optString("type", "OTHER")))
738
+ .withValue(ContactsContract.CommonDataKinds.Website.LABEL, url.optString("label"))
739
+ .build()
740
+ );
741
+ }
742
+ } catch (Exception ex) {
743
+ // Ignore
744
+ }
745
+ }
746
+
747
+ // Execute batch
748
+ android.content.ContentProviderResult[] results = resolver.applyBatch(ContactsContract.AUTHORITY, ops);
749
+
750
+ // Get the contact ID from the first result
751
+ if (results.length > 0 && results[0].uri != null) {
752
+ String rawContactId = results[0].uri.getLastPathSegment();
753
+ // Query for the actual contact ID
754
+ try (
755
+ Cursor cursor = resolver.query(
756
+ ContactsContract.RawContacts.CONTENT_URI,
757
+ new String[] { ContactsContract.RawContacts.CONTACT_ID },
758
+ ContactsContract.RawContacts._ID + " = ?",
759
+ new String[] { rawContactId },
760
+ null
761
+ )
762
+ ) {
763
+ if (cursor != null && cursor.moveToFirst()) {
764
+ return cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.CONTACT_ID));
765
+ }
766
+ }
767
+ }
768
+
769
+ throw new Exception("Failed to create contact");
770
+ }
771
+
772
+ @SuppressLint("Range")
773
+ private void updateContact(String contactId, JSObject contactData) throws Exception {
774
+ ContentResolver resolver = getContext().getContentResolver();
775
+ ArrayList<android.content.ContentProviderOperation> ops = new ArrayList<>();
776
+
777
+ // Find the raw contact ID for this contact
778
+ String rawContactId = null;
779
+ try (
780
+ Cursor cursor = resolver.query(
781
+ ContactsContract.RawContacts.CONTENT_URI,
782
+ new String[] { ContactsContract.RawContacts._ID },
783
+ ContactsContract.RawContacts.CONTACT_ID + " = ?",
784
+ new String[] { contactId },
785
+ null
786
+ )
787
+ ) {
788
+ if (cursor != null && cursor.moveToFirst()) {
789
+ rawContactId = cursor.getString(cursor.getColumnIndex(ContactsContract.RawContacts._ID));
790
+ }
791
+ }
792
+
793
+ if (rawContactId == null) {
794
+ throw new Exception("Contact not found");
795
+ }
796
+
797
+ // Delete existing data
798
+ ops.add(
799
+ android.content.ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI)
800
+ .withSelection(ContactsContract.Data.RAW_CONTACT_ID + " = ?", new String[] { rawContactId })
801
+ .build()
802
+ );
803
+
804
+ // Re-insert all data (simpler than updating individual fields)
805
+ int rawContactIdValue = Integer.parseInt(rawContactId);
806
+
807
+ // Add structured name
808
+ if (
809
+ contactData.has("givenName") ||
810
+ contactData.has("familyName") ||
811
+ contactData.has("middleName") ||
812
+ contactData.has("namePrefix") ||
813
+ contactData.has("nameSuffix")
814
+ ) {
815
+ ops.add(
816
+ android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
817
+ .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactIdValue)
818
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
819
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, contactData.getString("givenName"))
820
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, contactData.getString("familyName"))
821
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, contactData.getString("middleName"))
822
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.PREFIX, contactData.getString("namePrefix"))
823
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, contactData.getString("nameSuffix"))
824
+ .build()
825
+ );
826
+ }
827
+
828
+ // Similar logic for other fields (organization, note, emails, phones, etc.)
829
+ // Add organization
830
+ if (contactData.has("organizationName") || contactData.has("jobTitle")) {
831
+ ops.add(
832
+ android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
833
+ .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactIdValue)
834
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE)
835
+ .withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, contactData.getString("organizationName"))
836
+ .withValue(ContactsContract.CommonDataKinds.Organization.TITLE, contactData.getString("jobTitle"))
837
+ .build()
838
+ );
839
+ }
840
+
841
+ // Add note
842
+ if (contactData.has("note")) {
843
+ ops.add(
844
+ android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
845
+ .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactIdValue)
846
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE)
847
+ .withValue(ContactsContract.CommonDataKinds.Note.NOTE, contactData.getString("note"))
848
+ .build()
849
+ );
850
+ }
851
+
852
+ // Add emails, phones, addresses, URLs (same logic as insert)
853
+ if (contactData.has("emailAddresses")) {
854
+ try {
855
+ org.json.JSONArray emails = contactData.getJSONArray("emailAddresses");
856
+ for (int i = 0; i < emails.length(); i++) {
857
+ org.json.JSONObject email = emails.getJSONObject(i);
858
+ ops.add(
859
+ android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
860
+ .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactIdValue)
861
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
862
+ .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email.optString("value"))
863
+ .withValue(ContactsContract.CommonDataKinds.Email.TYPE, reverseMapEmailType(email.optString("type", "OTHER")))
864
+ .withValue(ContactsContract.CommonDataKinds.Email.LABEL, email.optString("label"))
865
+ .build()
866
+ );
867
+ }
868
+ } catch (Exception ex) {
869
+ // Ignore
870
+ }
871
+ }
872
+
873
+ if (contactData.has("phoneNumbers")) {
874
+ try {
875
+ org.json.JSONArray phones = contactData.getJSONArray("phoneNumbers");
876
+ for (int i = 0; i < phones.length(); i++) {
877
+ org.json.JSONObject phone = phones.getJSONObject(i);
878
+ ops.add(
879
+ android.content.ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
880
+ .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawContactIdValue)
881
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
882
+ .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone.optString("value"))
883
+ .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, reverseMapPhoneType(phone.optString("type", "OTHER")))
884
+ .withValue(ContactsContract.CommonDataKinds.Phone.LABEL, phone.optString("label"))
885
+ .build()
886
+ );
887
+ }
888
+ } catch (Exception ex) {
889
+ // Ignore
890
+ }
891
+ }
892
+
893
+ resolver.applyBatch(ContactsContract.AUTHORITY, ops);
894
+ }
895
+
896
+ private void populateIntent(Intent intent, JSObject contactData) {
897
+ // Build full name from components
898
+ StringBuilder nameBuilder = new StringBuilder();
899
+ if (contactData.has("givenName")) {
900
+ nameBuilder.append(contactData.getString("givenName"));
901
+ }
902
+ if (contactData.has("familyName")) {
903
+ if (nameBuilder.length() > 0) {
904
+ nameBuilder.append(" ");
905
+ }
906
+ nameBuilder.append(contactData.getString("familyName"));
907
+ }
908
+ if (nameBuilder.length() > 0) {
909
+ intent.putExtra(ContactsContract.Intents.Insert.NAME, nameBuilder.toString());
910
+ }
911
+
912
+ if (contactData.has("organizationName")) {
913
+ intent.putExtra(ContactsContract.Intents.Insert.COMPANY, contactData.getString("organizationName"));
914
+ }
915
+ if (contactData.has("jobTitle")) {
916
+ intent.putExtra(ContactsContract.Intents.Insert.JOB_TITLE, contactData.getString("jobTitle"));
917
+ }
918
+ if (contactData.has("note")) {
919
+ intent.putExtra(ContactsContract.Intents.Insert.NOTES, contactData.getString("note"));
920
+ }
921
+
922
+ // Add first email if available
923
+ if (contactData.has("emailAddresses")) {
924
+ try {
925
+ org.json.JSONArray emails = contactData.getJSONArray("emailAddresses");
926
+ if (emails.length() > 0) {
927
+ org.json.JSONObject email = emails.getJSONObject(0);
928
+ intent.putExtra(ContactsContract.Intents.Insert.EMAIL, email.optString("value"));
929
+ }
930
+ } catch (Exception ex) {
931
+ // Ignore
932
+ }
933
+ }
934
+
935
+ // Add first phone if available
936
+ if (contactData.has("phoneNumbers")) {
937
+ try {
938
+ org.json.JSONArray phones = contactData.getJSONArray("phoneNumbers");
939
+ if (phones.length() > 0) {
940
+ org.json.JSONObject phone = phones.getJSONObject(0);
941
+ intent.putExtra(ContactsContract.Intents.Insert.PHONE, phone.optString("value"));
942
+ }
943
+ } catch (Exception ex) {
944
+ // Ignore
945
+ }
946
+ }
947
+ }
948
+
949
+ private int reverseMapEmailType(String type) {
950
+ switch (type) {
951
+ case "HOME":
952
+ return ContactsContract.CommonDataKinds.Email.TYPE_HOME;
953
+ case "WORK":
954
+ return ContactsContract.CommonDataKinds.Email.TYPE_WORK;
955
+ case "MOBILE":
956
+ return ContactsContract.CommonDataKinds.Email.TYPE_MOBILE;
957
+ case "CUSTOM":
958
+ return ContactsContract.CommonDataKinds.Email.TYPE_CUSTOM;
959
+ default:
960
+ return ContactsContract.CommonDataKinds.Email.TYPE_OTHER;
961
+ }
962
+ }
963
+
964
+ private int reverseMapPhoneType(String type) {
965
+ switch (type) {
966
+ case "HOME":
967
+ return ContactsContract.CommonDataKinds.Phone.TYPE_HOME;
968
+ case "WORK":
969
+ return ContactsContract.CommonDataKinds.Phone.TYPE_WORK;
970
+ case "MOBILE":
971
+ return ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE;
972
+ case "MAIN":
973
+ return ContactsContract.CommonDataKinds.Phone.TYPE_MAIN;
974
+ case "HOME_FAX":
975
+ return ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME;
976
+ case "WORK_FAX":
977
+ return ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK;
978
+ case "OTHER_FAX":
979
+ return ContactsContract.CommonDataKinds.Phone.TYPE_OTHER_FAX;
980
+ case "PAGER":
981
+ return ContactsContract.CommonDataKinds.Phone.TYPE_PAGER;
982
+ case "CAR":
983
+ return ContactsContract.CommonDataKinds.Phone.TYPE_CAR;
984
+ case "CALLBACK":
985
+ return ContactsContract.CommonDataKinds.Phone.TYPE_CALLBACK;
986
+ case "COMPANY_MAIN":
987
+ return ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN;
988
+ case "ASSISTANT":
989
+ return ContactsContract.CommonDataKinds.Phone.TYPE_ASSISTANT;
990
+ case "CUSTOM":
991
+ return ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM;
992
+ default:
993
+ return ContactsContract.CommonDataKinds.Phone.TYPE_OTHER;
994
+ }
995
+ }
996
+
997
+ private int reverseMapPostalType(String type) {
998
+ switch (type) {
999
+ case "HOME":
1000
+ return ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME;
1001
+ case "WORK":
1002
+ return ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK;
1003
+ case "CUSTOM":
1004
+ return ContactsContract.CommonDataKinds.StructuredPostal.TYPE_CUSTOM;
1005
+ default:
1006
+ return ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER;
1007
+ }
1008
+ }
1009
+
1010
+ private int reverseMapUrlType(String type) {
1011
+ switch (type) {
1012
+ case "HOME":
1013
+ return ContactsContract.CommonDataKinds.Website.TYPE_HOME;
1014
+ case "WORK":
1015
+ return ContactsContract.CommonDataKinds.Website.TYPE_WORK;
1016
+ case "BLOG":
1017
+ return ContactsContract.CommonDataKinds.Website.TYPE_BLOG;
1018
+ case "PROFILE":
1019
+ return ContactsContract.CommonDataKinds.Website.TYPE_PROFILE;
1020
+ case "FTP":
1021
+ return ContactsContract.CommonDataKinds.Website.TYPE_FTP;
1022
+ case "CUSTOM":
1023
+ return ContactsContract.CommonDataKinds.Website.TYPE_CUSTOM;
1024
+ default:
1025
+ return ContactsContract.CommonDataKinds.Website.TYPE_OTHER;
1026
+ }
260
1027
  }
261
1028
 
262
1029
  // MARK: - Permissions helpers
@@ -265,6 +1032,10 @@ public class CapacitorContactsPlugin extends Plugin {
265
1032
  return getPermissionState("readContacts") == PermissionState.GRANTED;
266
1033
  }
267
1034
 
1035
+ private boolean hasWritePermission() {
1036
+ return getPermissionState("writeContacts") == PermissionState.GRANTED;
1037
+ }
1038
+
268
1039
  private JSObject buildPermissionStatus() {
269
1040
  JSObject status = new JSObject();
270
1041
  status.put("readContacts", mapPermissionState(getPermissionState("readContacts")));