@elizaos/capacitor-contacts 1.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.
- package/android/build.gradle +27 -0
- package/android/src/main/AndroidManifest.xml +5 -0
- package/android/src/main/java/ai/eliza/plugins/contacts/ContactsPlugin.kt +409 -0
- package/dist/esm/definitions.d.ts +38 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +14 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +13 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +28 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +31 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
ext {
|
|
2
|
+
junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
apply plugin: 'com.android.library'
|
|
6
|
+
android {
|
|
7
|
+
namespace = "ai.eliza.plugins.contacts"
|
|
8
|
+
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34
|
|
9
|
+
defaultConfig {
|
|
10
|
+
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
|
|
11
|
+
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 34
|
|
12
|
+
}
|
|
13
|
+
compileOptions {
|
|
14
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
15
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
repositories {
|
|
20
|
+
google()
|
|
21
|
+
maven { url = uri(rootProject.ext.mavenCentralMirrorUrl) }
|
|
22
|
+
mavenCentral()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
dependencies {
|
|
26
|
+
implementation project(':capacitor-android')
|
|
27
|
+
}
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
package ai.eliza.plugins.contacts
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.content.ContentProviderOperation
|
|
5
|
+
import android.provider.ContactsContract
|
|
6
|
+
import com.getcapacitor.JSArray
|
|
7
|
+
import com.getcapacitor.JSObject
|
|
8
|
+
import com.getcapacitor.Plugin
|
|
9
|
+
import com.getcapacitor.PluginCall
|
|
10
|
+
import com.getcapacitor.PluginMethod
|
|
11
|
+
import com.getcapacitor.annotation.CapacitorPlugin
|
|
12
|
+
|
|
13
|
+
@CapacitorPlugin(name = "ElizaContacts")
|
|
14
|
+
class ContactsPlugin : Plugin() {
|
|
15
|
+
@PluginMethod
|
|
16
|
+
fun listContacts(call: PluginCall) {
|
|
17
|
+
if (!hasPermission(Manifest.permission.READ_CONTACTS)) {
|
|
18
|
+
call.reject("READ_CONTACTS permission is required")
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
val query = call.getString("query")?.trim()?.lowercase()
|
|
23
|
+
val limit = call.getInt("limit") ?: 100
|
|
24
|
+
if (limit <= 0 || limit > 500) {
|
|
25
|
+
call.reject("limit must be between 1 and 500")
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
val contacts = JSArray()
|
|
29
|
+
val projection = arrayOf(
|
|
30
|
+
ContactsContract.Contacts._ID,
|
|
31
|
+
ContactsContract.Contacts.LOOKUP_KEY,
|
|
32
|
+
ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
|
|
33
|
+
ContactsContract.Contacts.PHOTO_THUMBNAIL_URI,
|
|
34
|
+
ContactsContract.Contacts.HAS_PHONE_NUMBER,
|
|
35
|
+
ContactsContract.Contacts.STARRED
|
|
36
|
+
)
|
|
37
|
+
val cursor = context.contentResolver.query(
|
|
38
|
+
ContactsContract.Contacts.CONTENT_URI,
|
|
39
|
+
projection,
|
|
40
|
+
null,
|
|
41
|
+
null,
|
|
42
|
+
"${ContactsContract.Contacts.DISPLAY_NAME_PRIMARY} ASC"
|
|
43
|
+
)
|
|
44
|
+
if (cursor == null) {
|
|
45
|
+
call.reject("Contacts provider returned no cursor")
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
cursor.use {
|
|
49
|
+
val idCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID)
|
|
50
|
+
val lookupCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.LOOKUP_KEY)
|
|
51
|
+
val nameCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY)
|
|
52
|
+
val photoCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.PHOTO_THUMBNAIL_URI)
|
|
53
|
+
val phoneCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.HAS_PHONE_NUMBER)
|
|
54
|
+
val starredCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.STARRED)
|
|
55
|
+
var count = 0
|
|
56
|
+
while (cursor.moveToNext() && count < limit) {
|
|
57
|
+
val id = cursor.getString(idCol)
|
|
58
|
+
val displayName = cursor.getString(nameCol) ?: ""
|
|
59
|
+
val phoneNumbers = readPhoneNumbers(id, cursor.getInt(phoneCol) > 0)
|
|
60
|
+
val emailAddresses = readEmailAddresses(id)
|
|
61
|
+
if (!matchesQuery(query, displayName, phoneNumbers, emailAddresses)) continue
|
|
62
|
+
contacts.put(
|
|
63
|
+
contactJson(
|
|
64
|
+
id = id,
|
|
65
|
+
lookupKey = cursor.getString(lookupCol) ?: "",
|
|
66
|
+
displayName = displayName,
|
|
67
|
+
photoUri = cursor.getString(photoCol),
|
|
68
|
+
phoneNumbers = phoneNumbers,
|
|
69
|
+
emailAddresses = emailAddresses,
|
|
70
|
+
starred = cursor.getInt(starredCol) == 1
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
count += 1
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
val result = JSObject()
|
|
78
|
+
result.put("contacts", contacts)
|
|
79
|
+
call.resolve(result)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@PluginMethod
|
|
83
|
+
fun createContact(call: PluginCall) {
|
|
84
|
+
if (!hasPermission(Manifest.permission.WRITE_CONTACTS)) {
|
|
85
|
+
call.reject("WRITE_CONTACTS permission is required")
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
val displayName = call.getString("displayName")?.trim()
|
|
89
|
+
if (displayName.isNullOrEmpty()) {
|
|
90
|
+
call.reject("displayName is required")
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
val phoneNumbers = readStringList(call, "phoneNumbers", call.getString("phoneNumber"))
|
|
94
|
+
val emailAddresses = readStringList(call, "emailAddresses", call.getString("emailAddress"))
|
|
95
|
+
val contactId = insertContact(displayName, phoneNumbers, emailAddresses)
|
|
96
|
+
val result = JSObject()
|
|
97
|
+
result.put("id", contactId)
|
|
98
|
+
call.resolve(result)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@PluginMethod
|
|
102
|
+
fun importVCard(call: PluginCall) {
|
|
103
|
+
if (!hasPermission(Manifest.permission.WRITE_CONTACTS)) {
|
|
104
|
+
call.reject("WRITE_CONTACTS permission is required")
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
val vcardText = call.getString("vcardText")
|
|
108
|
+
if (vcardText.isNullOrBlank()) {
|
|
109
|
+
call.reject("vcardText is required")
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
val parsedContacts = parseVCards(vcardText)
|
|
113
|
+
if (parsedContacts.isEmpty()) {
|
|
114
|
+
call.reject("No importable contacts were found in the vCard data")
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
val imported = JSArray()
|
|
119
|
+
for (parsed in parsedContacts) {
|
|
120
|
+
val contactId = insertContact(parsed.displayName, parsed.phoneNumbers, parsed.emailAddresses)
|
|
121
|
+
val summary = readContactSummary(contactId)
|
|
122
|
+
summary.put("sourceName", parsed.displayName)
|
|
123
|
+
imported.put(summary)
|
|
124
|
+
}
|
|
125
|
+
val result = JSObject()
|
|
126
|
+
result.put("imported", imported)
|
|
127
|
+
call.resolve(result)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private fun insertContact(
|
|
131
|
+
displayName: String,
|
|
132
|
+
phoneNumbers: List<String>,
|
|
133
|
+
emailAddresses: List<String>
|
|
134
|
+
): String {
|
|
135
|
+
val operations = ArrayList<ContentProviderOperation>()
|
|
136
|
+
operations.add(
|
|
137
|
+
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
|
|
138
|
+
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
|
|
139
|
+
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
|
|
140
|
+
.build()
|
|
141
|
+
)
|
|
142
|
+
operations.add(
|
|
143
|
+
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
|
|
144
|
+
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
|
|
145
|
+
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
|
|
146
|
+
.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName)
|
|
147
|
+
.build()
|
|
148
|
+
)
|
|
149
|
+
for (phoneNumber in phoneNumbers) {
|
|
150
|
+
operations.add(
|
|
151
|
+
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
|
|
152
|
+
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
|
|
153
|
+
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
|
|
154
|
+
.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber)
|
|
155
|
+
.withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE)
|
|
156
|
+
.build()
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
for (emailAddress in emailAddresses) {
|
|
160
|
+
operations.add(
|
|
161
|
+
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
|
|
162
|
+
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
|
|
163
|
+
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
|
|
164
|
+
.withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, emailAddress)
|
|
165
|
+
.withValue(ContactsContract.CommonDataKinds.Email.TYPE, ContactsContract.CommonDataKinds.Email.TYPE_OTHER)
|
|
166
|
+
.build()
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
val results = context.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations)
|
|
170
|
+
val rawContactId = results.firstOrNull()?.uri?.lastPathSegment
|
|
171
|
+
if (rawContactId.isNullOrEmpty()) {
|
|
172
|
+
throw IllegalStateException("Contacts provider did not return a raw contact id")
|
|
173
|
+
}
|
|
174
|
+
return resolveContactId(rawContactId)
|
|
175
|
+
?: throw IllegalStateException("Contacts provider did not link the inserted raw contact")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private fun resolveContactId(rawContactId: String): String? {
|
|
179
|
+
context.contentResolver.query(
|
|
180
|
+
ContactsContract.RawContacts.CONTENT_URI,
|
|
181
|
+
arrayOf(ContactsContract.RawContacts.CONTACT_ID),
|
|
182
|
+
"${ContactsContract.RawContacts._ID} = ?",
|
|
183
|
+
arrayOf(rawContactId),
|
|
184
|
+
null
|
|
185
|
+
)?.use { cursor ->
|
|
186
|
+
if (cursor.moveToFirst()) {
|
|
187
|
+
val contactIdCol = cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.CONTACT_ID)
|
|
188
|
+
return cursor.getString(contactIdCol)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private fun readContactSummary(contactId: String): JSObject {
|
|
195
|
+
val projection = arrayOf(
|
|
196
|
+
ContactsContract.Contacts._ID,
|
|
197
|
+
ContactsContract.Contacts.LOOKUP_KEY,
|
|
198
|
+
ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
|
|
199
|
+
ContactsContract.Contacts.PHOTO_THUMBNAIL_URI,
|
|
200
|
+
ContactsContract.Contacts.HAS_PHONE_NUMBER,
|
|
201
|
+
ContactsContract.Contacts.STARRED
|
|
202
|
+
)
|
|
203
|
+
val cursor = context.contentResolver.query(
|
|
204
|
+
ContactsContract.Contacts.CONTENT_URI,
|
|
205
|
+
projection,
|
|
206
|
+
"${ContactsContract.Contacts._ID} = ?",
|
|
207
|
+
arrayOf(contactId),
|
|
208
|
+
null
|
|
209
|
+
) ?: throw IllegalStateException("Contacts provider returned no cursor for $contactId")
|
|
210
|
+
cursor.use {
|
|
211
|
+
if (!cursor.moveToFirst()) {
|
|
212
|
+
throw IllegalStateException("Inserted contact $contactId was not readable")
|
|
213
|
+
}
|
|
214
|
+
val idCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID)
|
|
215
|
+
val lookupCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.LOOKUP_KEY)
|
|
216
|
+
val nameCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY)
|
|
217
|
+
val photoCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.PHOTO_THUMBNAIL_URI)
|
|
218
|
+
val phoneCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.HAS_PHONE_NUMBER)
|
|
219
|
+
val starredCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.STARRED)
|
|
220
|
+
val id = cursor.getString(idCol)
|
|
221
|
+
return contactJson(
|
|
222
|
+
id = id,
|
|
223
|
+
lookupKey = cursor.getString(lookupCol) ?: "",
|
|
224
|
+
displayName = cursor.getString(nameCol) ?: "",
|
|
225
|
+
photoUri = cursor.getString(photoCol),
|
|
226
|
+
phoneNumbers = readPhoneNumbers(id, cursor.getInt(phoneCol) > 0),
|
|
227
|
+
emailAddresses = readEmailAddresses(id),
|
|
228
|
+
starred = cursor.getInt(starredCol) == 1
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private fun readPhoneNumbers(contactId: String, hasPhone: Boolean): List<String> {
|
|
234
|
+
if (!hasPhone) return emptyList()
|
|
235
|
+
val numbers = mutableListOf<String>()
|
|
236
|
+
val cursor = context.contentResolver.query(
|
|
237
|
+
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
|
238
|
+
arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER),
|
|
239
|
+
"${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?",
|
|
240
|
+
arrayOf(contactId),
|
|
241
|
+
null
|
|
242
|
+
) ?: return numbers
|
|
243
|
+
cursor.use {
|
|
244
|
+
val numberCol = cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
|
245
|
+
while (cursor.moveToNext()) {
|
|
246
|
+
val number = cursor.getString(numberCol)?.trim()
|
|
247
|
+
if (!number.isNullOrEmpty()) numbers.add(number)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return numbers.distinct()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private fun readEmailAddresses(contactId: String): List<String> {
|
|
254
|
+
val emails = mutableListOf<String>()
|
|
255
|
+
val cursor = context.contentResolver.query(
|
|
256
|
+
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
|
|
257
|
+
arrayOf(ContactsContract.CommonDataKinds.Email.ADDRESS),
|
|
258
|
+
"${ContactsContract.CommonDataKinds.Email.CONTACT_ID} = ?",
|
|
259
|
+
arrayOf(contactId),
|
|
260
|
+
null
|
|
261
|
+
) ?: return emails
|
|
262
|
+
cursor.use {
|
|
263
|
+
val emailCol = cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Email.ADDRESS)
|
|
264
|
+
while (cursor.moveToNext()) {
|
|
265
|
+
val email = cursor.getString(emailCol)?.trim()
|
|
266
|
+
if (!email.isNullOrEmpty()) emails.add(email)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return emails.distinct()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private fun contactJson(
|
|
273
|
+
id: String,
|
|
274
|
+
lookupKey: String,
|
|
275
|
+
displayName: String,
|
|
276
|
+
photoUri: String?,
|
|
277
|
+
phoneNumbers: List<String>,
|
|
278
|
+
emailAddresses: List<String>,
|
|
279
|
+
starred: Boolean
|
|
280
|
+
): JSObject {
|
|
281
|
+
val contact = JSObject()
|
|
282
|
+
contact.put("id", id)
|
|
283
|
+
contact.put("lookupKey", lookupKey)
|
|
284
|
+
contact.put("displayName", displayName)
|
|
285
|
+
contact.put("photoUri", photoUri)
|
|
286
|
+
contact.put("phoneNumbers", JSArray(phoneNumbers))
|
|
287
|
+
contact.put("emailAddresses", JSArray(emailAddresses))
|
|
288
|
+
contact.put("starred", starred)
|
|
289
|
+
return contact
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private fun matchesQuery(
|
|
293
|
+
query: String?,
|
|
294
|
+
displayName: String,
|
|
295
|
+
phoneNumbers: List<String>,
|
|
296
|
+
emailAddresses: List<String>
|
|
297
|
+
): Boolean {
|
|
298
|
+
if (query.isNullOrEmpty()) return true
|
|
299
|
+
if (displayName.lowercase().contains(query)) return true
|
|
300
|
+
if (phoneNumbers.any { it.lowercase().contains(query) }) return true
|
|
301
|
+
return emailAddresses.any { it.lowercase().contains(query) }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private fun readStringList(call: PluginCall, arrayKey: String, singleValue: String?): List<String> {
|
|
305
|
+
val values = mutableListOf<String>()
|
|
306
|
+
val array = call.getArray(arrayKey)
|
|
307
|
+
if (array != null) {
|
|
308
|
+
for (index in 0 until array.length()) {
|
|
309
|
+
val value = array.optString(index).trim()
|
|
310
|
+
if (value.isNotEmpty()) values.add(value)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
val single = singleValue?.trim()
|
|
314
|
+
if (!single.isNullOrEmpty()) values.add(single)
|
|
315
|
+
return values.distinct()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private fun parseVCards(input: String): List<ParsedVCard> {
|
|
319
|
+
val unfolded = unfoldVCardLines(input)
|
|
320
|
+
val contacts = mutableListOf<ParsedVCard>()
|
|
321
|
+
var current = mutableListOf<String>()
|
|
322
|
+
var insideCard = false
|
|
323
|
+
for (line in unfolded) {
|
|
324
|
+
val upper = line.uppercase()
|
|
325
|
+
if (upper == "BEGIN:VCARD") {
|
|
326
|
+
insideCard = true
|
|
327
|
+
current = mutableListOf()
|
|
328
|
+
} else if (upper == "END:VCARD") {
|
|
329
|
+
if (insideCard) {
|
|
330
|
+
parseVCard(current)?.let { contacts.add(it) }
|
|
331
|
+
}
|
|
332
|
+
insideCard = false
|
|
333
|
+
current = mutableListOf()
|
|
334
|
+
} else if (insideCard) {
|
|
335
|
+
current.add(line)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (contacts.isEmpty()) {
|
|
339
|
+
parseVCard(unfolded)?.let { contacts.add(it) }
|
|
340
|
+
}
|
|
341
|
+
return contacts
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private fun unfoldVCardLines(input: String): List<String> {
|
|
345
|
+
val lines = mutableListOf<String>()
|
|
346
|
+
for (rawLine in input.replace("\r\n", "\n").replace('\r', '\n').split('\n')) {
|
|
347
|
+
if ((rawLine.startsWith(" ") || rawLine.startsWith("\t")) && lines.isNotEmpty()) {
|
|
348
|
+
lines[lines.lastIndex] = lines.last() + rawLine.drop(1)
|
|
349
|
+
} else {
|
|
350
|
+
lines.add(rawLine.trimEnd())
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return lines
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private fun parseVCard(lines: List<String>): ParsedVCard? {
|
|
357
|
+
var fullName: String? = null
|
|
358
|
+
var structuredName: String? = null
|
|
359
|
+
val phoneNumbers = mutableListOf<String>()
|
|
360
|
+
val emailAddresses = mutableListOf<String>()
|
|
361
|
+
for (line in lines) {
|
|
362
|
+
val separator = line.indexOf(':')
|
|
363
|
+
if (separator <= 0) continue
|
|
364
|
+
val key = line.substring(0, separator).substringBefore(';').uppercase()
|
|
365
|
+
val value = decodeVCardValue(line.substring(separator + 1)).trim()
|
|
366
|
+
if (value.isEmpty()) continue
|
|
367
|
+
when (key) {
|
|
368
|
+
"FN" -> fullName = value
|
|
369
|
+
"N" -> structuredName = structuredNameToDisplayName(value)
|
|
370
|
+
"TEL" -> phoneNumbers.add(value)
|
|
371
|
+
"EMAIL" -> emailAddresses.add(value)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
val displayName = fullName ?: structuredName ?: phoneNumbers.firstOrNull() ?: emailAddresses.firstOrNull()
|
|
375
|
+
if (displayName.isNullOrBlank()) return null
|
|
376
|
+
return ParsedVCard(
|
|
377
|
+
displayName = displayName,
|
|
378
|
+
phoneNumbers = phoneNumbers.map { it.trim() }.filter { it.isNotEmpty() }.distinct(),
|
|
379
|
+
emailAddresses = emailAddresses.map { it.trim() }.filter { it.isNotEmpty() }.distinct()
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private fun structuredNameToDisplayName(value: String): String {
|
|
384
|
+
val parts = value.split(';').map { decodeVCardValue(it).trim() }
|
|
385
|
+
val family = parts.getOrNull(0).orEmpty()
|
|
386
|
+
val given = parts.getOrNull(1).orEmpty()
|
|
387
|
+
val additional = parts.getOrNull(2).orEmpty()
|
|
388
|
+
val prefix = parts.getOrNull(3).orEmpty()
|
|
389
|
+
val suffix = parts.getOrNull(4).orEmpty()
|
|
390
|
+
return listOf(prefix, given, additional, family, suffix)
|
|
391
|
+
.filter { it.isNotEmpty() }
|
|
392
|
+
.joinToString(" ")
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private fun decodeVCardValue(value: String): String {
|
|
396
|
+
return value
|
|
397
|
+
.replace("\\n", "\n")
|
|
398
|
+
.replace("\\N", "\n")
|
|
399
|
+
.replace("\\,", ",")
|
|
400
|
+
.replace("\\;", ";")
|
|
401
|
+
.replace("\\\\", "\\")
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private data class ParsedVCard(
|
|
405
|
+
val displayName: String,
|
|
406
|
+
val phoneNumbers: List<String>,
|
|
407
|
+
val emailAddresses: List<String>
|
|
408
|
+
)
|
|
409
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface ContactSummary {
|
|
2
|
+
id: string;
|
|
3
|
+
lookupKey: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
phoneNumbers: string[];
|
|
6
|
+
emailAddresses: string[];
|
|
7
|
+
photoUri?: string;
|
|
8
|
+
starred: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ListContactsOptions {
|
|
11
|
+
query?: string;
|
|
12
|
+
limit?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface CreateContactOptions {
|
|
15
|
+
displayName: string;
|
|
16
|
+
phoneNumber?: string;
|
|
17
|
+
phoneNumbers?: string[];
|
|
18
|
+
emailAddress?: string;
|
|
19
|
+
emailAddresses?: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface ImportVCardOptions {
|
|
22
|
+
vcardText: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ImportedContactSummary extends ContactSummary {
|
|
25
|
+
sourceName: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ContactsPlugin {
|
|
28
|
+
listContacts(options?: ListContactsOptions): Promise<{
|
|
29
|
+
contacts: ContactSummary[];
|
|
30
|
+
}>;
|
|
31
|
+
createContact(options: CreateContactOptions): Promise<{
|
|
32
|
+
id: string;
|
|
33
|
+
}>;
|
|
34
|
+
importVCard(options: ImportVCardOptions): Promise<{
|
|
35
|
+
imported: ImportedContactSummary[];
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=definitions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAuB,SAAQ,cAAc;IAC5D,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,YAAY,CACV,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC;QAAE,QAAQ,EAAE,cAAc,EAAE,CAAA;KAAE,CAAC,CAAC;IAC3C,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtE,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC;QAChD,QAAQ,EAAE,sBAAsB,EAAE,CAAC;KACpC,CAAC,CAAC;CACJ"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,cAAc,eAAe,CAAC;AAI9B,eAAO,MAAM,QAAQ,gBAEnB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { registerPlugin } from "@capacitor/core";
|
|
2
|
+
export * from "./definitions";
|
|
3
|
+
const loadWeb = () => import("./web").then((m) => new m.ContactsWeb());
|
|
4
|
+
export const Contacts = registerPlugin("ElizaContacts", {
|
|
5
|
+
web: loadWeb,
|
|
6
|
+
});
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAIjD,cAAc,eAAe,CAAC;AAE9B,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;AAEvE,MAAM,CAAC,MAAM,QAAQ,GAAG,cAAc,CAAiB,eAAe,EAAE;IACtE,GAAG,EAAE,OAAO;CACb,CAAC,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { WebPlugin } from "@capacitor/core";
|
|
2
|
+
import type { ContactSummary, ContactsPlugin, CreateContactOptions, ImportedContactSummary, ImportVCardOptions, ListContactsOptions } from "./definitions";
|
|
3
|
+
export declare class ContactsWeb extends WebPlugin implements ContactsPlugin {
|
|
4
|
+
listContacts(_options?: ListContactsOptions): Promise<{
|
|
5
|
+
contacts: ContactSummary[];
|
|
6
|
+
}>;
|
|
7
|
+
createContact(_options: CreateContactOptions): Promise<{
|
|
8
|
+
id: string;
|
|
9
|
+
}>;
|
|
10
|
+
importVCard(_options: ImportVCardOptions): Promise<{
|
|
11
|
+
imported: ImportedContactSummary[];
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,sBAAsB,EACtB,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,eAAe,CAAC;AAEvB,qBAAa,WAAY,SAAQ,SAAU,YAAW,cAAc;IAC5D,YAAY,CAChB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC;QAAE,QAAQ,EAAE,cAAc,EAAE,CAAA;KAAE,CAAC;IAIpC,aAAa,CAAC,QAAQ,EAAE,oBAAoB,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAItE,WAAW,CACf,QAAQ,EAAE,kBAAkB,GAC3B,OAAO,CAAC;QAAE,QAAQ,EAAE,sBAAsB,EAAE,CAAA;KAAE,CAAC;CAGnD"}
|
package/dist/esm/web.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { WebPlugin } from "@capacitor/core";
|
|
2
|
+
export class ContactsWeb extends WebPlugin {
|
|
3
|
+
async listContacts(_options) {
|
|
4
|
+
return { contacts: [] };
|
|
5
|
+
}
|
|
6
|
+
async createContact(_options) {
|
|
7
|
+
throw new Error("Contacts are only available on Android.");
|
|
8
|
+
}
|
|
9
|
+
async importVCard(_options) {
|
|
10
|
+
throw new Error("Contact imports are only available on Android.");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAW5C,MAAM,OAAO,WAAY,SAAQ,SAAS;IACxC,KAAK,CAAC,YAAY,CAChB,QAA8B;QAE9B,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,QAA8B;QAChD,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,WAAW,CACf,QAA4B;QAE5B,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;CACF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@capacitor/core');
|
|
4
|
+
|
|
5
|
+
const loadWeb = () => Promise.resolve().then(function () { return web; }).then((m) => new m.ContactsWeb());
|
|
6
|
+
const Contacts = core.registerPlugin("ElizaContacts", {
|
|
7
|
+
web: loadWeb,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
class ContactsWeb extends core.WebPlugin {
|
|
11
|
+
async listContacts(_options) {
|
|
12
|
+
return { contacts: [] };
|
|
13
|
+
}
|
|
14
|
+
async createContact(_options) {
|
|
15
|
+
throw new Error("Contacts are only available on Android.");
|
|
16
|
+
}
|
|
17
|
+
async importVCard(_options) {
|
|
18
|
+
throw new Error("Contact imports are only available on Android.");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
var web = /*#__PURE__*/Object.freeze({
|
|
23
|
+
__proto__: null,
|
|
24
|
+
ContactsWeb: ContactsWeb
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
exports.Contacts = Contacts;
|
|
28
|
+
//# sourceMappingURL=plugin.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from \"@capacitor/core\";\nexport * from \"./definitions\";\nconst loadWeb = () => import(\"./web\").then((m) => new m.ContactsWeb());\nexport const Contacts = registerPlugin(\"ElizaContacts\", {\n web: loadWeb,\n});\n//# sourceMappingURL=index.js.map","import { WebPlugin } from \"@capacitor/core\";\nexport class ContactsWeb extends WebPlugin {\n async listContacts(_options) {\n return { contacts: [] };\n }\n async createContact(_options) {\n throw new Error(\"Contacts are only available on Android.\");\n }\n async importVCard(_options) {\n throw new Error(\"Contact imports are only available on Android.\");\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AAEA,MAAM,OAAO,GAAG,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;AAC1D,MAAC,QAAQ,GAAGA,mBAAc,CAAC,eAAe,EAAE;AACxD,IAAI,GAAG,EAAE,OAAO;AAChB,CAAC;;ACJM,MAAM,WAAW,SAASC,cAAS,CAAC;AAC3C,IAAI,MAAM,YAAY,CAAC,QAAQ,EAAE;AACjC,QAAQ,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE;AAC/B,IAAI;AACJ,IAAI,MAAM,aAAa,CAAC,QAAQ,EAAE;AAClC,QAAQ,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC;AAClE,IAAI;AACJ,IAAI,MAAM,WAAW,CAAC,QAAQ,EAAE;AAChC,QAAQ,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC;AACzE,IAAI;AACJ;;;;;;;;;"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
var capacitorContacts = (function (exports, core) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const loadWeb = () => Promise.resolve().then(function () { return web; }).then((m) => new m.ContactsWeb());
|
|
5
|
+
const Contacts = core.registerPlugin("ElizaContacts", {
|
|
6
|
+
web: loadWeb,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
class ContactsWeb extends core.WebPlugin {
|
|
10
|
+
async listContacts(_options) {
|
|
11
|
+
return { contacts: [] };
|
|
12
|
+
}
|
|
13
|
+
async createContact(_options) {
|
|
14
|
+
throw new Error("Contacts are only available on Android.");
|
|
15
|
+
}
|
|
16
|
+
async importVCard(_options) {
|
|
17
|
+
throw new Error("Contact imports are only available on Android.");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var web = /*#__PURE__*/Object.freeze({
|
|
22
|
+
__proto__: null,
|
|
23
|
+
ContactsWeb: ContactsWeb
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
exports.Contacts = Contacts;
|
|
27
|
+
|
|
28
|
+
return exports;
|
|
29
|
+
|
|
30
|
+
})({}, capacitorExports);
|
|
31
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from \"@capacitor/core\";\nexport * from \"./definitions\";\nconst loadWeb = () => import(\"./web\").then((m) => new m.ContactsWeb());\nexport const Contacts = registerPlugin(\"ElizaContacts\", {\n web: loadWeb,\n});\n//# sourceMappingURL=index.js.map","import { WebPlugin } from \"@capacitor/core\";\nexport class ContactsWeb extends WebPlugin {\n async listContacts(_options) {\n return { contacts: [] };\n }\n async createContact(_options) {\n throw new Error(\"Contacts are only available on Android.\");\n }\n async importVCard(_options) {\n throw new Error(\"Contact imports are only available on Android.\");\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;IAEA,MAAM,OAAO,GAAG,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;AAC1D,UAAC,QAAQ,GAAGA,mBAAc,CAAC,eAAe,EAAE;IACxD,IAAI,GAAG,EAAE,OAAO;IAChB,CAAC;;ICJM,MAAM,WAAW,SAASC,cAAS,CAAC;IAC3C,IAAI,MAAM,YAAY,CAAC,QAAQ,EAAE;IACjC,QAAQ,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE;IAC/B,IAAI;IACJ,IAAI,MAAM,aAAa,CAAC,QAAQ,EAAE;IAClC,QAAQ,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC;IAClE,IAAI;IACJ,IAAI,MAAM,WAAW,CAAC,QAAQ,EAAE;IAChC,QAAQ,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC;IACzE,IAAI;IACJ;;;;;;;;;;;;;;;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elizaos/capacitor-contacts",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Android ContactsContract bridge for ElizaOS.",
|
|
5
|
+
"main": "./dist/plugin.cjs.js",
|
|
6
|
+
"module": "./dist/esm/index.js",
|
|
7
|
+
"types": "./dist/esm/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/esm/index.d.ts",
|
|
11
|
+
"import": "./dist/esm/index.js",
|
|
12
|
+
"require": "./dist/plugin.cjs.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"android/src/main/",
|
|
18
|
+
"android/build.gradle",
|
|
19
|
+
"dist/"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "npm run clean && tsc && rollup -c rollup.config.mjs",
|
|
23
|
+
"clean": "rimraf ./dist",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"capacitor": {
|
|
28
|
+
"android": {
|
|
29
|
+
"src": "android"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@capacitor/cli": "^8.0.0",
|
|
34
|
+
"@capacitor/core": "^8.3.1",
|
|
35
|
+
"rimraf": "^6.0.0",
|
|
36
|
+
"rollup": "^4.60.2",
|
|
37
|
+
"typescript": "^6.0.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@capacitor/core": "^8.3.1"
|
|
41
|
+
},
|
|
42
|
+
"elizaos": {
|
|
43
|
+
"platforms": [
|
|
44
|
+
"browser",
|
|
45
|
+
"node"
|
|
46
|
+
],
|
|
47
|
+
"runtime": "both",
|
|
48
|
+
"platformDetails": {
|
|
49
|
+
"browser": "Web fallback returns an empty contacts list.",
|
|
50
|
+
"android": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|