@capgo/capacitor-contacts 7.0.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.
@@ -0,0 +1,770 @@
1
+ package app.capgo.contacts;
2
+
3
+ import android.Manifest;
4
+ import android.annotation.SuppressLint;
5
+ import android.content.ContentResolver;
6
+ import android.content.ContentUris;
7
+ import android.content.Intent;
8
+ import android.database.Cursor;
9
+ import android.net.Uri;
10
+ import android.provider.ContactsContract;
11
+ import android.provider.Settings;
12
+ import android.util.Base64;
13
+ import androidx.annotation.NonNull;
14
+ import com.getcapacitor.JSArray;
15
+ import com.getcapacitor.JSObject;
16
+ import com.getcapacitor.PermissionState;
17
+ import com.getcapacitor.Plugin;
18
+ import com.getcapacitor.PluginCall;
19
+ import com.getcapacitor.PluginMethod;
20
+ import com.getcapacitor.annotation.CapacitorPlugin;
21
+ import com.getcapacitor.annotation.Permission;
22
+ import com.getcapacitor.annotation.PermissionCallback;
23
+ import java.io.ByteArrayOutputStream;
24
+ import java.io.IOException;
25
+ import java.io.InputStream;
26
+ import java.util.ArrayList;
27
+ import java.util.HashMap;
28
+ import java.util.HashSet;
29
+ import java.util.List;
30
+ import java.util.Map;
31
+ import java.util.Set;
32
+
33
+ @CapacitorPlugin(
34
+ name = "CapacitorContacts",
35
+ permissions = {
36
+ @Permission(alias = "readContacts", strings = { Manifest.permission.READ_CONTACTS }),
37
+ @Permission(alias = "writeContacts", strings = { Manifest.permission.WRITE_CONTACTS })
38
+ }
39
+ )
40
+ public class CapacitorContactsPlugin extends Plugin {
41
+
42
+ // MARK: - Implemented API surface
43
+
44
+ @PluginMethod
45
+ public void countContacts(PluginCall call) {
46
+ if (!hasReadPermission()) {
47
+ call.reject("READ_CONTACTS permission not granted.");
48
+ return;
49
+ }
50
+
51
+ ContentResolver resolver = getContext().getContentResolver();
52
+ try (
53
+ Cursor cursor = resolver.query(
54
+ ContactsContract.Contacts.CONTENT_URI,
55
+ new String[] { ContactsContract.Contacts._ID },
56
+ null,
57
+ null,
58
+ null
59
+ )
60
+ ) {
61
+ int count = cursor != null ? cursor.getCount() : 0;
62
+ JSObject result = new JSObject();
63
+ result.put("count", count);
64
+ call.resolve(result);
65
+ } catch (Exception ex) {
66
+ call.reject("Failed to count contacts.", null, ex);
67
+ }
68
+ }
69
+
70
+ @PluginMethod
71
+ public void getContacts(PluginCall call) {
72
+ if (!hasReadPermission()) {
73
+ call.reject("READ_CONTACTS permission not granted.");
74
+ return;
75
+ }
76
+
77
+ JSObject options = call.getObject("options", new JSObject());
78
+ Integer limit = options.has("limit") ? options.getInteger("limit") : null;
79
+ Integer offset = options.has("offset") ? options.getInteger("offset") : null;
80
+
81
+ try {
82
+ List<ContactBuilder> builders = fetchContacts(limit, offset);
83
+ JSArray contacts = new JSArray();
84
+ for (ContactBuilder builder : builders) {
85
+ contacts.put(builder.toJSObject());
86
+ }
87
+ JSObject result = new JSObject();
88
+ result.put("contacts", contacts);
89
+ call.resolve(result);
90
+ } catch (Exception ex) {
91
+ call.reject("Failed to fetch contacts.", null, ex);
92
+ }
93
+ }
94
+
95
+ @PluginMethod
96
+ public void getContactById(PluginCall call) {
97
+ if (!hasReadPermission()) {
98
+ call.reject("READ_CONTACTS permission not granted.");
99
+ return;
100
+ }
101
+
102
+ JSObject options = call.getObject("options", new JSObject());
103
+ String identifier = options.getString("id");
104
+ if (identifier == null) {
105
+ call.reject("Missing contact identifier.");
106
+ return;
107
+ }
108
+
109
+ try {
110
+ ContactBuilder builder = fetchContact(identifier);
111
+ if (builder == null) {
112
+ call.resolve(new JSObject().put("contact", null));
113
+ return;
114
+ }
115
+ JSObject result = new JSObject();
116
+ result.put("contact", builder.toJSObject());
117
+ call.resolve(result);
118
+ } catch (Exception ex) {
119
+ call.reject("Failed to fetch contact.", null, ex);
120
+ }
121
+ }
122
+
123
+ @PluginMethod
124
+ public void getAccounts(PluginCall call) {
125
+ ContentResolver resolver = getContext().getContentResolver();
126
+ JSArray accounts = new JSArray();
127
+ Set<String> seen = new HashSet<>();
128
+
129
+ try (
130
+ Cursor cursor = resolver.query(
131
+ ContactsContract.RawContacts.CONTENT_URI,
132
+ new String[] { ContactsContract.RawContacts.ACCOUNT_NAME, ContactsContract.RawContacts.ACCOUNT_TYPE },
133
+ null,
134
+ null,
135
+ null
136
+ )
137
+ ) {
138
+ if (cursor != null) {
139
+ while (cursor.moveToNext()) {
140
+ String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.ACCOUNT_NAME));
141
+ String type = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.ACCOUNT_TYPE));
142
+ String key = (name == null ? "" : name) + "|" + (type == null ? "" : type);
143
+ if (seen.add(key)) {
144
+ JSObject account = new JSObject();
145
+ account.put("name", name);
146
+ account.put("type", type);
147
+ accounts.put(account);
148
+ }
149
+ }
150
+ }
151
+ call.resolve(new JSObject().put("accounts", accounts));
152
+ } catch (Exception ex) {
153
+ call.reject("Failed to fetch accounts.", null, ex);
154
+ }
155
+ }
156
+
157
+ @PluginMethod
158
+ public void isSupported(PluginCall call) {
159
+ call.resolve(new JSObject().put("isSupported", true));
160
+ }
161
+
162
+ @PluginMethod
163
+ public void isAvailable(PluginCall call) {
164
+ call.resolve(new JSObject().put("isAvailable", true));
165
+ }
166
+
167
+ @PluginMethod
168
+ public void openSettings(PluginCall call) {
169
+ Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
170
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
171
+ Uri uri = Uri.fromParts("package", getContext().getPackageName(), null);
172
+ intent.setData(uri);
173
+ getContext().startActivity(intent);
174
+ call.resolve();
175
+ }
176
+
177
+ @PluginMethod
178
+ public void checkPermissions(PluginCall call) {
179
+ JSObject status = buildPermissionStatus();
180
+ call.resolve(status);
181
+ }
182
+
183
+ @PluginMethod
184
+ public void requestPermissions(PluginCall call) {
185
+ // In Capacitor 7, the permission system works differently
186
+ // We just need to request the permissions defined in the plugin annotation
187
+ requestPermissionForAlias("readContacts", call, "handleRequestPermissions");
188
+ }
189
+
190
+ @PermissionCallback
191
+ public void handleRequestPermissions(PluginCall call) {
192
+ JSObject status = buildPermissionStatus();
193
+ call.resolve(status);
194
+ }
195
+
196
+ // MARK: - Not yet implemented operations
197
+
198
+ private void notImplemented(PluginCall call) {
199
+ call.reject("Method not implemented yet.");
200
+ }
201
+
202
+ @PluginMethod
203
+ public void createContact(PluginCall call) {
204
+ notImplemented(call);
205
+ }
206
+
207
+ @PluginMethod
208
+ public void createGroup(PluginCall call) {
209
+ notImplemented(call);
210
+ }
211
+
212
+ @PluginMethod
213
+ public void deleteContactById(PluginCall call) {
214
+ notImplemented(call);
215
+ }
216
+
217
+ @PluginMethod
218
+ public void deleteGroupById(PluginCall call) {
219
+ notImplemented(call);
220
+ }
221
+
222
+ @PluginMethod
223
+ public void displayContactById(PluginCall call) {
224
+ notImplemented(call);
225
+ }
226
+
227
+ @PluginMethod
228
+ public void displayCreateContact(PluginCall call) {
229
+ notImplemented(call);
230
+ }
231
+
232
+ @PluginMethod
233
+ public void displayUpdateContactById(PluginCall call) {
234
+ notImplemented(call);
235
+ }
236
+
237
+ @PluginMethod
238
+ public void getGroupById(PluginCall call) {
239
+ notImplemented(call);
240
+ }
241
+
242
+ @PluginMethod
243
+ public void getGroups(PluginCall call) {
244
+ notImplemented(call);
245
+ }
246
+
247
+ @PluginMethod
248
+ public void pickContact(PluginCall call) {
249
+ notImplemented(call);
250
+ }
251
+
252
+ @PluginMethod
253
+ public void pickContacts(PluginCall call) {
254
+ notImplemented(call);
255
+ }
256
+
257
+ @PluginMethod
258
+ public void updateContactById(PluginCall call) {
259
+ notImplemented(call);
260
+ }
261
+
262
+ // MARK: - Permissions helpers
263
+
264
+ private boolean hasReadPermission() {
265
+ return getPermissionState("readContacts") == PermissionState.GRANTED;
266
+ }
267
+
268
+ private JSObject buildPermissionStatus() {
269
+ JSObject status = new JSObject();
270
+ status.put("readContacts", mapPermissionState(getPermissionState("readContacts")));
271
+ status.put("writeContacts", mapPermissionState(getPermissionState("writeContacts")));
272
+ return status;
273
+ }
274
+
275
+ private String mapPermissionState(PermissionState state) {
276
+ if (state == null) {
277
+ return "prompt";
278
+ }
279
+ switch (state) {
280
+ case GRANTED:
281
+ return "granted";
282
+ case DENIED:
283
+ return "denied";
284
+ case PROMPT_WITH_RATIONALE:
285
+ return "prompt-with-rationale";
286
+ default:
287
+ return "prompt";
288
+ }
289
+ }
290
+
291
+ // MARK: - Contact access helpers
292
+
293
+ private List<ContactBuilder> fetchContacts(Integer limit, Integer offset) {
294
+ List<ContactBuilder> builders = new ArrayList<>();
295
+ ContentResolver resolver = getContext().getContentResolver();
296
+
297
+ Uri queryUri = ContactsContract.Contacts.CONTENT_URI;
298
+ if (limit != null) {
299
+ android.net.Uri.Builder builder = ContactsContract.Contacts.CONTENT_URI.buildUpon();
300
+ builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, String.valueOf(limit));
301
+ if (offset != null) {
302
+ // START_PARAM_KEY was removed, use "offset" directly
303
+ builder.appendQueryParameter("offset", String.valueOf(offset));
304
+ }
305
+ queryUri = builder.build();
306
+ }
307
+
308
+ try (
309
+ Cursor cursor = resolver.query(
310
+ queryUri,
311
+ new String[] { ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME_PRIMARY },
312
+ null,
313
+ null,
314
+ ContactsContract.Contacts.DISPLAY_NAME_PRIMARY + " ASC"
315
+ )
316
+ ) {
317
+ if (cursor == null) {
318
+ return builders;
319
+ }
320
+
321
+ while (cursor.moveToNext()) {
322
+ String id = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID));
323
+ ContactBuilder contact = fetchContact(id);
324
+ if (contact != null) {
325
+ contact.fullName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY));
326
+ builders.add(contact);
327
+ }
328
+ }
329
+ }
330
+
331
+ return builders;
332
+ }
333
+
334
+ private ContactBuilder fetchContact(@NonNull String contactId) {
335
+ ContentResolver resolver = getContext().getContentResolver();
336
+ ContactBuilder builder = new ContactBuilder(contactId);
337
+
338
+ // Structured name, emails, phones, etc.
339
+ try (
340
+ Cursor dataCursor = resolver.query(
341
+ ContactsContract.Data.CONTENT_URI,
342
+ null,
343
+ ContactsContract.Data.CONTACT_ID + " = ?",
344
+ new String[] { contactId },
345
+ null
346
+ )
347
+ ) {
348
+ if (dataCursor == null) {
349
+ return null;
350
+ }
351
+
352
+ while (dataCursor.moveToNext()) {
353
+ String mimeType = dataCursor.getString(dataCursor.getColumnIndexOrThrow(ContactsContract.Data.MIMETYPE));
354
+ switch (mimeType) {
355
+ case ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE:
356
+ builder.givenName = dataCursor.getString(
357
+ dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)
358
+ );
359
+ builder.familyName = dataCursor.getString(
360
+ dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)
361
+ );
362
+ builder.middleName = dataCursor.getString(
363
+ dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME)
364
+ );
365
+ builder.namePrefix = dataCursor.getString(
366
+ dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.PREFIX)
367
+ );
368
+ builder.nameSuffix = dataCursor.getString(
369
+ dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredName.SUFFIX)
370
+ );
371
+ break;
372
+ case ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE:
373
+ builder.addEmail(
374
+ dataCursor.getString(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.ADDRESS)),
375
+ dataCursor.getInt(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.TYPE)),
376
+ dataCursor.getString(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.LABEL)),
377
+ dataCursor.getInt(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.IS_PRIMARY)) == 1
378
+ );
379
+ break;
380
+ case ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE:
381
+ builder.addPhone(
382
+ dataCursor.getString(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER)),
383
+ dataCursor.getInt(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.TYPE)),
384
+ dataCursor.getString(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL)),
385
+ dataCursor.getInt(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.IS_PRIMARY)) == 1
386
+ );
387
+ break;
388
+ case ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE:
389
+ builder.addPostalAddress(
390
+ dataCursor.getInt(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.TYPE)),
391
+ dataCursor.getString(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.LABEL)),
392
+ dataCursor.getString(
393
+ dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.STREET)
394
+ ),
395
+ dataCursor.getString(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.CITY)),
396
+ dataCursor.getString(
397
+ dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.REGION)
398
+ ),
399
+ dataCursor.getString(
400
+ dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE)
401
+ ),
402
+ dataCursor.getString(
403
+ dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY)
404
+ ),
405
+ dataCursor.getString(
406
+ dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD)
407
+ ),
408
+ dataCursor.getInt(
409
+ dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.StructuredPostal.IS_PRIMARY)
410
+ ) ==
411
+ 1
412
+ );
413
+ break;
414
+ case ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE:
415
+ builder.addUrlAddress(
416
+ dataCursor.getString(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Website.URL)),
417
+ dataCursor.getInt(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Website.TYPE)),
418
+ dataCursor.getString(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Website.LABEL))
419
+ );
420
+ break;
421
+ case ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE:
422
+ builder.organizationName = dataCursor.getString(
423
+ dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Organization.COMPANY)
424
+ );
425
+ builder.jobTitle = dataCursor.getString(
426
+ dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Organization.TITLE)
427
+ );
428
+ break;
429
+ case ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE:
430
+ builder.note = dataCursor.getString(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Note.NOTE));
431
+ break;
432
+ case ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE:
433
+ int eventType = dataCursor.getInt(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Event.TYPE));
434
+ if (eventType == ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY) {
435
+ builder.setBirthday(
436
+ dataCursor.getString(dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Event.START_DATE))
437
+ );
438
+ }
439
+ break;
440
+ case ContactsContract.CommonDataKinds.GroupMembership.CONTENT_ITEM_TYPE:
441
+ long groupId = dataCursor.getLong(
442
+ dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID)
443
+ );
444
+ builder.addGroupId(String.valueOf(groupId));
445
+ break;
446
+ case ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE:
447
+ byte[] photoData = dataCursor.getBlob(
448
+ dataCursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Photo.PHOTO)
449
+ );
450
+ if (photoData != null) {
451
+ builder.photoBase64 = Base64.encodeToString(photoData, Base64.NO_WRAP);
452
+ }
453
+ break;
454
+ default:
455
+ break;
456
+ }
457
+ }
458
+ }
459
+
460
+ // Account information
461
+ try (
462
+ Cursor rawCursor = resolver.query(
463
+ ContactsContract.RawContacts.CONTENT_URI,
464
+ new String[] { ContactsContract.RawContacts.ACCOUNT_NAME, ContactsContract.RawContacts.ACCOUNT_TYPE },
465
+ ContactsContract.RawContacts.CONTACT_ID + " = ?",
466
+ new String[] { contactId },
467
+ null
468
+ )
469
+ ) {
470
+ if (rawCursor != null && rawCursor.moveToFirst()) {
471
+ String accountName = rawCursor.getString(rawCursor.getColumnIndexOrThrow(ContactsContract.RawContacts.ACCOUNT_NAME));
472
+ String accountType = rawCursor.getString(rawCursor.getColumnIndexOrThrow(ContactsContract.RawContacts.ACCOUNT_TYPE));
473
+ builder.accountName = accountName;
474
+ builder.accountType = accountType;
475
+ }
476
+ }
477
+
478
+ if (builder.fullName == null) {
479
+ builder.fullName = resolveDisplayName(contactId);
480
+ }
481
+
482
+ return builder;
483
+ }
484
+
485
+ private String resolveDisplayName(String contactId) {
486
+ ContentResolver resolver = getContext().getContentResolver();
487
+ try (
488
+ Cursor cursor = resolver.query(
489
+ ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, Long.parseLong(contactId)),
490
+ new String[] { ContactsContract.Contacts.DISPLAY_NAME_PRIMARY },
491
+ null,
492
+ null,
493
+ null
494
+ )
495
+ ) {
496
+ if (cursor != null && cursor.moveToFirst()) {
497
+ return cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY));
498
+ }
499
+ }
500
+ return null;
501
+ }
502
+
503
+ // MARK: - Contact builder helper
504
+
505
+ private static class ContactBuilder {
506
+
507
+ final String id;
508
+ String givenName;
509
+ String familyName;
510
+ String middleName;
511
+ String namePrefix;
512
+ String nameSuffix;
513
+ String organizationName;
514
+ String jobTitle;
515
+ String note;
516
+ String fullName;
517
+ String photoBase64;
518
+ String accountName;
519
+ String accountType;
520
+ Integer birthdayYear;
521
+ Integer birthdayMonth;
522
+ Integer birthdayDay;
523
+ final JSArray groupIds = new JSArray();
524
+ final JSArray emailAddresses = new JSArray();
525
+ final JSArray phoneNumbers = new JSArray();
526
+ final JSArray postalAddresses = new JSArray();
527
+ final JSArray urlAddresses = new JSArray();
528
+
529
+ ContactBuilder(String id) {
530
+ this.id = id;
531
+ }
532
+
533
+ void addEmail(String value, int type, String label, boolean isPrimary) {
534
+ if (value == null) {
535
+ return;
536
+ }
537
+ JSObject email = new JSObject();
538
+ email.put("value", value);
539
+ email.put("type", mapEmailType(type));
540
+ if (type == ContactsContract.CommonDataKinds.Email.TYPE_CUSTOM && label != null) {
541
+ email.put("label", label);
542
+ }
543
+ email.put("isPrimary", isPrimary);
544
+ emailAddresses.put(email);
545
+ }
546
+
547
+ void addPhone(String value, int type, String label, boolean isPrimary) {
548
+ if (value == null) {
549
+ return;
550
+ }
551
+ JSObject phone = new JSObject();
552
+ phone.put("value", value);
553
+ phone.put("type", mapPhoneType(type));
554
+ if (type == ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM && label != null) {
555
+ phone.put("label", label);
556
+ }
557
+ phone.put("isPrimary", isPrimary);
558
+ phoneNumbers.put(phone);
559
+ }
560
+
561
+ void addPostalAddress(
562
+ int type,
563
+ String label,
564
+ String street,
565
+ String city,
566
+ String state,
567
+ String postalCode,
568
+ String country,
569
+ String neighborhood,
570
+ boolean isPrimary
571
+ ) {
572
+ JSObject address = new JSObject();
573
+ address.put("street", street);
574
+ address.put("city", city);
575
+ address.put("state", state);
576
+ address.put("postalCode", postalCode);
577
+ address.put("country", country);
578
+ address.put("neighborhood", neighborhood);
579
+ address.put("formatted", buildFormattedAddress(street, city, state, postalCode, country));
580
+ address.put("isoCountryCode", null);
581
+ address.put("isPrimary", isPrimary);
582
+ address.put("type", mapPostalType(type));
583
+ if (type == ContactsContract.CommonDataKinds.StructuredPostal.TYPE_CUSTOM && label != null) {
584
+ address.put("label", label);
585
+ }
586
+ postalAddresses.put(address);
587
+ }
588
+
589
+ void addUrlAddress(String value, int type, String label) {
590
+ if (value == null) {
591
+ return;
592
+ }
593
+ JSObject url = new JSObject();
594
+ url.put("value", value);
595
+ url.put("type", mapUrlType(type));
596
+ if (type == ContactsContract.CommonDataKinds.Website.TYPE_CUSTOM && label != null) {
597
+ url.put("label", label);
598
+ }
599
+ urlAddresses.put(url);
600
+ }
601
+
602
+ void addGroupId(String groupId) {
603
+ if (groupId == null) {
604
+ return;
605
+ }
606
+ groupIds.put(groupId);
607
+ }
608
+
609
+ void setBirthday(String startDate) {
610
+ if (startDate == null || startDate.isEmpty()) {
611
+ return;
612
+ }
613
+ String[] parts = startDate.split("-");
614
+ if (parts.length >= 2) {
615
+ birthdayMonth = safeParse(parts[0]);
616
+ birthdayDay = safeParse(parts[1]);
617
+ }
618
+ if (parts.length >= 3) {
619
+ birthdayYear = safeParse(parts[2]);
620
+ }
621
+ }
622
+
623
+ JSObject toJSObject() {
624
+ JSObject contact = new JSObject();
625
+ contact.put("id", id);
626
+ contact.put("givenName", givenName);
627
+ contact.put("familyName", familyName);
628
+ contact.put("middleName", middleName);
629
+ contact.put("namePrefix", namePrefix);
630
+ contact.put("nameSuffix", nameSuffix);
631
+ contact.put("organizationName", organizationName);
632
+ contact.put("jobTitle", jobTitle);
633
+ contact.put("note", note);
634
+ contact.put("fullName", fullName);
635
+ contact.put("photo", photoBase64);
636
+ contact.put("groupIds", groupIds);
637
+ contact.put("emailAddresses", emailAddresses);
638
+ contact.put("phoneNumbers", phoneNumbers);
639
+ contact.put("postalAddresses", postalAddresses);
640
+ contact.put("urlAddresses", urlAddresses);
641
+
642
+ if (birthdayYear != null || birthdayMonth != null || birthdayDay != null) {
643
+ JSObject birthday = new JSObject();
644
+ if (birthdayDay != null) birthday.put("day", birthdayDay);
645
+ if (birthdayMonth != null) birthday.put("month", birthdayMonth);
646
+ if (birthdayYear != null) birthday.put("year", birthdayYear);
647
+ contact.put("birthday", birthday);
648
+ }
649
+
650
+ if (accountName != null || accountType != null) {
651
+ JSObject account = new JSObject();
652
+ account.put("name", accountName);
653
+ account.put("type", accountType);
654
+ contact.put("account", account);
655
+ } else {
656
+ contact.put("account", null);
657
+ }
658
+
659
+ return contact;
660
+ }
661
+
662
+ private static Integer safeParse(String value) {
663
+ try {
664
+ return Integer.parseInt(value);
665
+ } catch (Exception ex) {
666
+ return null;
667
+ }
668
+ }
669
+
670
+ private static String buildFormattedAddress(String street, String city, String state, String postalCode, String country) {
671
+ StringBuilder builder = new StringBuilder();
672
+ if (street != null && !street.isEmpty()) builder.append(street).append('\n');
673
+ if (city != null && !city.isEmpty()) builder.append(city);
674
+ if (state != null && !state.isEmpty()) builder.append(builder.length() > 0 ? ", " : "").append(state);
675
+ if (postalCode != null && !postalCode.isEmpty()) builder.append(' ').append(postalCode);
676
+ if (country != null && !country.isEmpty()) builder.append(builder.length() > 0 ? "\n" : "").append(country);
677
+ return builder.toString();
678
+ }
679
+
680
+ private static String mapEmailType(int type) {
681
+ switch (type) {
682
+ case ContactsContract.CommonDataKinds.Email.TYPE_HOME:
683
+ return "HOME";
684
+ case ContactsContract.CommonDataKinds.Email.TYPE_WORK:
685
+ return "WORK";
686
+ case ContactsContract.CommonDataKinds.Email.TYPE_OTHER:
687
+ return "OTHER";
688
+ case ContactsContract.CommonDataKinds.Email.TYPE_MOBILE:
689
+ return "MOBILE";
690
+ case ContactsContract.CommonDataKinds.Email.TYPE_CUSTOM:
691
+ return "CUSTOM";
692
+ // TYPE_MAIN constant was removed from Android SDK
693
+ default:
694
+ return "OTHER";
695
+ }
696
+ }
697
+
698
+ private static String mapPhoneType(int type) {
699
+ switch (type) {
700
+ case ContactsContract.CommonDataKinds.Phone.TYPE_HOME:
701
+ return "HOME";
702
+ case ContactsContract.CommonDataKinds.Phone.TYPE_WORK:
703
+ return "WORK";
704
+ case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE:
705
+ return "MOBILE";
706
+ case ContactsContract.CommonDataKinds.Phone.TYPE_MAIN:
707
+ return "MAIN";
708
+ case ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME:
709
+ return "HOME_FAX";
710
+ case ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK:
711
+ return "WORK_FAX";
712
+ case ContactsContract.CommonDataKinds.Phone.TYPE_OTHER_FAX:
713
+ return "OTHER_FAX";
714
+ case ContactsContract.CommonDataKinds.Phone.TYPE_PAGER:
715
+ return "PAGER";
716
+ case ContactsContract.CommonDataKinds.Phone.TYPE_CAR:
717
+ return "CAR";
718
+ case ContactsContract.CommonDataKinds.Phone.TYPE_CALLBACK:
719
+ return "CALLBACK";
720
+ case ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN:
721
+ return "COMPANY_MAIN";
722
+ case ContactsContract.CommonDataKinds.Phone.TYPE_ASSISTANT:
723
+ return "ASSISTANT";
724
+ case ContactsContract.CommonDataKinds.Phone.TYPE_OTHER:
725
+ return "OTHER";
726
+ case ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM:
727
+ return "CUSTOM";
728
+ default:
729
+ return "OTHER";
730
+ }
731
+ }
732
+
733
+ private static String mapPostalType(int type) {
734
+ switch (type) {
735
+ case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME:
736
+ return "HOME";
737
+ case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK:
738
+ return "WORK";
739
+ case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER:
740
+ return "OTHER";
741
+ case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_CUSTOM:
742
+ return "CUSTOM";
743
+ default:
744
+ return "OTHER";
745
+ }
746
+ }
747
+
748
+ private static String mapUrlType(int type) {
749
+ switch (type) {
750
+ case ContactsContract.CommonDataKinds.Website.TYPE_HOME:
751
+ return "HOME";
752
+ case ContactsContract.CommonDataKinds.Website.TYPE_WORK:
753
+ return "WORK";
754
+ case ContactsContract.CommonDataKinds.Website.TYPE_BLOG:
755
+ return "BLOG";
756
+ case ContactsContract.CommonDataKinds.Website.TYPE_PROFILE:
757
+ return "PROFILE";
758
+ case ContactsContract.CommonDataKinds.Website.TYPE_FTP:
759
+ return "FTP";
760
+ // TYPE_HOME_PAGE and TYPE_SCHOOL constants were removed from Android SDK
761
+ case ContactsContract.CommonDataKinds.Website.TYPE_OTHER:
762
+ return "OTHER";
763
+ case ContactsContract.CommonDataKinds.Website.TYPE_CUSTOM:
764
+ return "CUSTOM";
765
+ default:
766
+ return "OTHER";
767
+ }
768
+ }
769
+ }
770
+ }