From 02cec92d72e9c5be281fdeab2133b1a3c2680604 Mon Sep 17 00:00:00 2001
From: MM20 <15646950+MM2-0@users.noreply.github.com>
Date: Sun, 6 Apr 2025 12:53:38 +0200
Subject: [PATCH] Implement contact plugins
---
.idea/emulatorDisplays.xml | 47 --------
.../java/de/mm20/launcher2/search/Contact.kt | 3 +
.../plugin/contracts/ContactPluginContract.kt | 24 ++++
data/contacts/build.gradle.kts | 1 +
.../contacts/ContactSerialization.kt | 114 +++++++++++++++++-
.../java/de/mm20/launcher2/contacts/Module.kt | 4 +-
.../contacts/providers/AndroidContact.kt | 5 +-
.../providers/AndroidContactProvider.kt | 3 +-
.../contacts/providers/ContactProvider.kt | 2 +-
.../contacts/providers/PluginContact.kt | 85 +++++++++++++
.../providers/PluginContactProvider.kt | 82 +++++++++++++
.../launcher2/files/providers/PluginFile.kt | 3 +-
.../de/mm20/launcher2/sdk/contacts/Contact.kt | 5 +
.../launcher2/sdk/contacts/ContactProvider.kt | 2 +
14 files changed, 321 insertions(+), 59 deletions(-)
delete mode 100644 .idea/emulatorDisplays.xml
create mode 100644 data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/PluginContact.kt
create mode 100644 data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/PluginContactProvider.kt
diff --git a/.idea/emulatorDisplays.xml b/.idea/emulatorDisplays.xml
deleted file mode 100644
index 1bb36d9e..00000000
--- a/.idea/emulatorDisplays.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/core/base/src/main/java/de/mm20/launcher2/search/Contact.kt b/core/base/src/main/java/de/mm20/launcher2/search/Contact.kt
index 2f0b3a4b..6861fc08 100644
--- a/core/base/src/main/java/de/mm20/launcher2/search/Contact.kt
+++ b/core/base/src/main/java/de/mm20/launcher2/search/Contact.kt
@@ -19,6 +19,9 @@ interface Contact : SavableSearchable {
val postalAddresses: List
val customActions: List
+ override val label: String
+ get() = name
+
val summary: String
get() {
return (phoneNumbers.map { it.number } + emailAddresses.map { it.address })
diff --git a/core/shared/src/main/java/de/mm20/launcher2/plugin/contracts/ContactPluginContract.kt b/core/shared/src/main/java/de/mm20/launcher2/plugin/contracts/ContactPluginContract.kt
index 8fab2c8f..dfa9d64a 100644
--- a/core/shared/src/main/java/de/mm20/launcher2/plugin/contracts/ContactPluginContract.kt
+++ b/core/shared/src/main/java/de/mm20/launcher2/plugin/contracts/ContactPluginContract.kt
@@ -12,17 +12,41 @@ abstract class ContactPluginContract {
*/
val Id = column("id")
+ /**
+ * Uri to view the contact.
+ */
+ val Uri = column("uri")
+
/**
* The display name of the contact.
* First name + last name, if applicable.
*/
val Name = column("name")
+ /**
+ * List of phone numbers associated with the contact.
+ */
val PhoneNumbers = column>("phone_numbers")
+
+
+ /**
+ * List of email addresses associated with the contact.
+ */
val EmailAddresses = column>("email_addresses")
+
+ /**
+ * List of postal addresses associated with the contact.
+ */
val PostalAddresses = column>("postal_addresses")
+
+ /**
+ * List of custom actions associated with the contact.
+ */
val CustomActions = column>("custom_actions")
+ /**
+ * Uri to the contact's photo.
+ */
val PhotoUri = column("photo_uri")
}
}
\ No newline at end of file
diff --git a/data/contacts/build.gradle.kts b/data/contacts/build.gradle.kts
index e3e120c1..1e66927c 100644
--- a/data/contacts/build.gradle.kts
+++ b/data/contacts/build.gradle.kts
@@ -40,6 +40,7 @@ dependencies {
implementation(libs.koin.android)
+ implementation(libs.coil.core)
implementation(project(":core:ktx"))
implementation(project(":core:base"))
diff --git a/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactSerialization.kt b/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactSerialization.kt
index 591e04f9..b8dcde86 100644
--- a/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactSerialization.kt
+++ b/data/contacts/src/main/java/de/mm20/launcher2/contacts/ContactSerialization.kt
@@ -1,18 +1,46 @@
package de.mm20.launcher2.contacts
import android.content.Context
+import androidx.core.net.toUri
import de.mm20.launcher2.contacts.providers.AndroidContact
import de.mm20.launcher2.contacts.providers.AndroidContactProvider
+import de.mm20.launcher2.contacts.providers.PluginContact
+import de.mm20.launcher2.contacts.providers.PluginContactProvider
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
+import de.mm20.launcher2.plugin.PluginRepository
+import de.mm20.launcher2.plugin.config.StorageStrategy
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
-import kotlinx.coroutines.flow.first
+import de.mm20.launcher2.search.UpdateResult
+import de.mm20.launcher2.search.asUpdateResult
+import de.mm20.launcher2.search.contact.CustomContactAction
+import de.mm20.launcher2.search.contact.EmailAddress
+import de.mm20.launcher2.search.contact.PhoneNumber
+import de.mm20.launcher2.search.contact.PostalAddress
+import de.mm20.launcher2.serialization.Json
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.serialization.Serializable
import org.json.JSONObject
-internal class ContactSerializer : SearchableSerializer {
+@Serializable
+internal data class SerializedPluginContact(
+ val id: String? = null,
+ val uri: String? = null,
+ val name: String? = null,
+ val phoneNumbers: List? = null,
+ val emailAddresses: List? = null,
+ val postalAddresses: List? = null,
+ val customActions: List? = null,
+ val photoUri: String? = null,
+ val authority: String? = null,
+ val strategy: StorageStrategy = StorageStrategy.StoreCopy,
+ val timestamp: Long = 0L,
+)
+
+internal class AndroidContactSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String {
searchable as AndroidContact
return jsonObjectOf(
@@ -21,10 +49,45 @@ internal class ContactSerializer : SearchableSerializer {
}
override val typePrefix: String
- get() = "contact"
+ get() = AndroidContact.Domain
}
-internal class ContactDeserializer(
+
+internal class PluginContactSerializer : SearchableSerializer {
+ override fun serialize(searchable: SavableSearchable): String {
+ searchable as PluginContact
+ if (searchable.storageStrategy == StorageStrategy.StoreReference) {
+ return Json.Lenient.encodeToString(
+ SerializedPluginContact(
+ id = searchable.id,
+ authority = searchable.authority,
+ strategy = searchable.storageStrategy,
+ )
+ )
+ } else {
+ return Json.Lenient.encodeToString(
+ SerializedPluginContact(
+ id = searchable.id,
+ uri = searchable.uri.toString(),
+ name = searchable.name,
+ phoneNumbers = searchable.phoneNumbers,
+ emailAddresses = searchable.emailAddresses,
+ postalAddresses = searchable.postalAddresses,
+ customActions = searchable.customActions,
+ photoUri = searchable.photoUri?.toString(),
+ authority = searchable.authority,
+ strategy = searchable.storageStrategy,
+ timestamp = searchable.timestamp,
+ )
+ )
+ }
+ }
+
+ override val typePrefix: String
+ get() = PluginContact.Domain
+}
+
+internal class AndroidContactDeserializer(
private val context: Context,
private val permissionsManager: PermissionsManager
) : SearchableDeserializer {
@@ -37,4 +100,47 @@ internal class ContactDeserializer(
return androidContactProvider.get(id)
}
+}
+
+
+internal class PluginContactDeserializer(
+ private val context: Context,
+ private val pluginRepository: PluginRepository,
+): SearchableDeserializer {
+ override suspend fun deserialize(serialized: String): SavableSearchable? {
+ val json = Json.Lenient.decodeFromString(serialized)
+ val authority = json.authority ?: return null
+ val id = json.id ?: return null
+ val strategy = json.strategy
+ val plugin = pluginRepository.get(authority).firstOrNull() ?: return null
+ if (!plugin.enabled) return null
+
+ return when(strategy) {
+ StorageStrategy.StoreReference -> {
+ PluginContactProvider(context, authority).get(id).getOrNull()
+ }
+
+ else -> {
+ val timestamp = json.timestamp
+
+ PluginContact(
+ id = id,
+ name = json.name ?: return null,
+ uri = json.uri?.toUri() ?: return null,
+ phoneNumbers = json.phoneNumbers ?: emptyList(),
+ emailAddresses = json.emailAddresses ?: emptyList(),
+ postalAddresses = json.postalAddresses ?: emptyList(),
+ customActions = json.customActions ?: emptyList(),
+ photoUri = json.photoUri?.toUri(),
+ authority = authority,
+ storageStrategy = strategy,
+ timestamp = timestamp,
+ updatedSelf = {
+ if (it !is PluginContact) UpdateResult.TemporarilyUnavailable()
+ else PluginContactProvider(context, authority).refresh(it, timestamp).asUpdateResult()
+ }
+ )
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/data/contacts/src/main/java/de/mm20/launcher2/contacts/Module.kt b/data/contacts/src/main/java/de/mm20/launcher2/contacts/Module.kt
index 3776a916..19060ae2 100644
--- a/data/contacts/src/main/java/de/mm20/launcher2/contacts/Module.kt
+++ b/data/contacts/src/main/java/de/mm20/launcher2/contacts/Module.kt
@@ -1,6 +1,7 @@
package de.mm20.launcher2.contacts
import de.mm20.launcher2.contacts.providers.AndroidContact
+import de.mm20.launcher2.contacts.providers.PluginContact
import de.mm20.launcher2.search.Contact
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableRepository
@@ -11,5 +12,6 @@ import org.koin.dsl.module
val contactsModule = module {
factory { ContactRepository(androidContext(), get(), get()) }
factory>(named()) { get() }
- factory(named(AndroidContact.Domain)) { ContactDeserializer(androidContext(), get()) }
+ factory(named(AndroidContact.Domain)) { AndroidContactDeserializer(androidContext(), get()) }
+ factory(named(PluginContact.Domain)) { PluginContactDeserializer(androidContext(), get()) }
}
\ No newline at end of file
diff --git a/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/AndroidContact.kt b/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/AndroidContact.kt
index 3ecb388e..7d45ba0c 100644
--- a/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/AndroidContact.kt
+++ b/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/AndroidContact.kt
@@ -6,7 +6,7 @@ import android.content.Intent
import android.os.Bundle
import android.provider.ContactsContract
import androidx.core.graphics.drawable.toDrawable
-import de.mm20.launcher2.contacts.ContactSerializer
+import de.mm20.launcher2.contacts.AndroidContactSerializer
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.StaticIconLayer
@@ -36,7 +36,6 @@ internal data class AndroidContact(
override val domain: String = Domain
override val key: String
get() = "$Domain://$id"
- override val label: String = name
override val summary: String
get() {
@@ -76,7 +75,7 @@ internal data class AndroidContact(
}
override fun getSerializer(): SearchableSerializer {
- return ContactSerializer()
+ return AndroidContactSerializer()
}
companion object {
diff --git a/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/AndroidContactProvider.kt b/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/AndroidContactProvider.kt
index 7c961343..bb6ee9ad 100644
--- a/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/AndroidContactProvider.kt
+++ b/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/AndroidContactProvider.kt
@@ -7,7 +7,6 @@ import android.provider.ContactsContract
import android.telephony.PhoneNumberUtils
import androidx.core.database.getLongOrNull
import androidx.core.database.getStringOrNull
-import de.mm20.launcher2.contacts.providers.AndroidContact
import de.mm20.launcher2.ktx.distinctByEquality
import de.mm20.launcher2.search.Contact
import de.mm20.launcher2.search.contact.ContactInfoType
@@ -21,7 +20,7 @@ import kotlinx.coroutines.withContext
/**
* A contact provider that uses the Android ContactsContract API to search for contacts.
*/
-class AndroidContactProvider(
+internal class AndroidContactProvider(
private val context: Context,
) : ContactProvider {
override suspend fun search(
diff --git a/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/ContactProvider.kt b/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/ContactProvider.kt
index 03ce9006..77fb1e1e 100644
--- a/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/ContactProvider.kt
+++ b/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/ContactProvider.kt
@@ -2,6 +2,6 @@ package de.mm20.launcher2.contacts.providers
import de.mm20.launcher2.search.Contact
-interface ContactProvider {
+internal interface ContactProvider {
suspend fun search(query: String, allowNetwork: Boolean): List
}
\ No newline at end of file
diff --git a/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/PluginContact.kt b/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/PluginContact.kt
new file mode 100644
index 00000000..b15d03c7
--- /dev/null
+++ b/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/PluginContact.kt
@@ -0,0 +1,85 @@
+package de.mm20.launcher2.contacts.providers
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import coil.imageLoader
+import coil.request.ImageRequest
+import de.mm20.launcher2.contacts.PluginContactSerializer
+import de.mm20.launcher2.icons.ColorLayer
+import de.mm20.launcher2.icons.LauncherIcon
+import de.mm20.launcher2.icons.StaticIconLayer
+import de.mm20.launcher2.icons.StaticLauncherIcon
+import de.mm20.launcher2.ktx.tryStartActivity
+import de.mm20.launcher2.plugin.config.StorageStrategy
+import de.mm20.launcher2.search.Contact
+import de.mm20.launcher2.search.File
+import de.mm20.launcher2.search.SavableSearchable
+import de.mm20.launcher2.search.SearchableSerializer
+import de.mm20.launcher2.search.UpdatableSearchable
+import de.mm20.launcher2.search.UpdateResult
+import de.mm20.launcher2.search.contact.CustomContactAction
+import de.mm20.launcher2.search.contact.EmailAddress
+import de.mm20.launcher2.search.contact.PhoneNumber
+import de.mm20.launcher2.search.contact.PostalAddress
+
+internal data class PluginContact(
+ val id: String,
+ val uri: Uri,
+ override val name: String,
+ override val phoneNumbers: List,
+ override val emailAddresses: List,
+ override val postalAddresses: List,
+ override val customActions: List,
+ val photoUri: Uri?,
+ override val labelOverride: String? = null,
+ val authority: String,
+ val storageStrategy: StorageStrategy,
+ override val timestamp: Long,
+ override val updatedSelf: (suspend (SavableSearchable) -> UpdateResult)?
+) : Contact, UpdatableSearchable {
+ override val domain: String = Domain
+ override fun getSerializer(): SearchableSerializer {
+ return PluginContactSerializer()
+ }
+
+ override val key: String = "$domain://$authority:$id"
+ override fun overrideLabel(label: String): SavableSearchable {
+ return copy(labelOverride = label)
+ }
+
+ override fun launch(
+ context: Context,
+ options: Bundle?
+ ): Boolean {
+ return context.tryStartActivity(
+ Intent(
+ Intent.ACTION_VIEW
+ ).apply {
+ data = uri
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }, options
+ )
+ }
+
+ override suspend fun loadIcon(context: Context, size: Int, themed: Boolean): LauncherIcon? {
+ if (photoUri != null) {
+ val request = ImageRequest.Builder(context)
+ .data(photoUri)
+ .size(size)
+ .build()
+ val result = context.imageLoader.execute(request)
+ val drawable = result.drawable ?: return null
+ return StaticLauncherIcon(
+ foregroundLayer = StaticIconLayer(icon = drawable),
+ backgroundLayer = ColorLayer(),
+ )
+ }
+ return super.loadIcon(context, size, themed)
+ }
+
+ companion object {
+ const val Domain = "plugin.contact"
+ }
+}
\ No newline at end of file
diff --git a/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/PluginContactProvider.kt b/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/PluginContactProvider.kt
new file mode 100644
index 00000000..60b7b1c2
--- /dev/null
+++ b/data/contacts/src/main/java/de/mm20/launcher2/contacts/providers/PluginContactProvider.kt
@@ -0,0 +1,82 @@
+package de.mm20.launcher2.contacts.providers
+
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import de.mm20.launcher2.plugin.PluginApi
+import de.mm20.launcher2.plugin.QueryPluginApi
+import de.mm20.launcher2.plugin.config.QueryPluginConfig
+import de.mm20.launcher2.plugin.contracts.ContactPluginContract
+import de.mm20.launcher2.plugin.contracts.ContactPluginContract.ContactColumns
+import de.mm20.launcher2.plugin.contracts.SearchPluginContract
+import de.mm20.launcher2.plugin.data.set
+import de.mm20.launcher2.plugin.data.withColumns
+import de.mm20.launcher2.search.UpdateResult
+import de.mm20.launcher2.search.asUpdateResult
+
+internal class PluginContactProvider(
+ private val context: Context,
+ private val authority: String,
+): QueryPluginApi(context, authority), ContactProvider {
+
+ private fun getPluginConfig(): QueryPluginConfig? {
+ return PluginApi(authority, context.contentResolver).getSearchPluginConfig()
+ }
+
+ override fun Uri.Builder.appendQueryParameters(query: String): Uri.Builder {
+ return appendQueryParameter(SearchPluginContract.Params.Query, query)
+ }
+
+ override fun Cursor.getData(): List? {
+ val config = getPluginConfig()
+ val cursor = this
+
+ if (config == null) {
+ Log.e("MM20", "Plugin $authority returned null config")
+ cursor.close()
+ return null
+ }
+
+ val results = mutableListOf()
+ val timestamp = System.currentTimeMillis()
+ cursor.withColumns(ContactColumns) {
+ while (cursor.moveToNext()) {
+ results.add(
+ PluginContact(
+ id = cursor[ContactColumns.Id] ?: continue,
+ name = cursor[ContactColumns.Name] ?: continue,
+ uri = Uri.parse(cursor[ContactColumns.Uri] ?: continue),
+ phoneNumbers = cursor[ContactColumns.PhoneNumbers] ?: emptyList(),
+ emailAddresses = cursor[ContactColumns.EmailAddresses] ?: emptyList(),
+ postalAddresses = cursor[ContactColumns.PostalAddresses] ?: emptyList(),
+ customActions = cursor[ContactColumns.CustomActions] ?: emptyList(),
+ photoUri = cursor[ContactColumns.PhotoUri]?.let { Uri.parse(it) },
+ authority = authority,
+ storageStrategy = config.storageStrategy,
+ timestamp = timestamp,
+ updatedSelf = {
+ if (it !is PluginContact) UpdateResult.TemporarilyUnavailable()
+ else refresh(it, timestamp).asUpdateResult()
+ }
+ )
+ )
+ }
+ }
+ return results
+ }
+
+ override fun PluginContact.toBundle(): Bundle {
+ return Bundle().apply {
+ set(ContactColumns.Id, id)
+ set(ContactColumns.Uri, uri.toString())
+ set(ContactColumns.Name, name)
+ set(ContactColumns.PhoneNumbers, phoneNumbers)
+ set(ContactColumns.EmailAddresses, emailAddresses)
+ set(ContactColumns.PostalAddresses, postalAddresses)
+ set(ContactColumns.CustomActions, customActions)
+ set(ContactColumns.PhotoUri, photoUri?.toString())
+ }
+ }
+}
\ No newline at end of file
diff --git a/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFile.kt b/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFile.kt
index ee958ef6..875a022f 100644
--- a/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFile.kt
+++ b/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFile.kt
@@ -70,11 +70,12 @@ data class PluginFile(
if (thumbnailUri != null) {
val request = ImageRequest.Builder(context)
.data(thumbnailUri)
+ .size(size)
.build()
val result = context.imageLoader.execute(request)
val drawable = result.drawable ?: return null
return StaticLauncherIcon(
- foregroundLayer = StaticIconLayer(icon = drawable, scale = 1.5f),
+ foregroundLayer = StaticIconLayer(icon = drawable),
backgroundLayer = ColorLayer(),
)
}
diff --git a/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/contacts/Contact.kt b/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/contacts/Contact.kt
index 5cbcf497..ad8f1820 100644
--- a/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/contacts/Contact.kt
+++ b/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/contacts/Contact.kt
@@ -11,6 +11,11 @@ data class Contact(
* A unique and stable identifier for this contact.
*/
val id: String,
+
+ /**
+ * The URI to view this contact.
+ */
+ val uri: Uri,
/**
* The display name for this contact.
* First name + last name, if applicable.
diff --git a/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/contacts/ContactProvider.kt b/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/contacts/ContactProvider.kt
index 7d04fcbf..4747d2ed 100644
--- a/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/contacts/ContactProvider.kt
+++ b/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/contacts/ContactProvider.kt
@@ -22,6 +22,7 @@ abstract class ContactProvider(
override fun List.toCursor(): Cursor {
return buildCursor(ContactColumns, this) {
put(ContactColumns.Id, it.id)
+ put(ContactColumns.Uri, it.uri.toString())
put(ContactColumns.Name, it.name)
put(ContactColumns.PhoneNumbers, it.phoneNumbers)
put(ContactColumns.EmailAddresses, it.emailAddresses)
@@ -35,6 +36,7 @@ abstract class ContactProvider(
return Contact(
id = get(ContactColumns.Id) ?: return null,
name = get(ContactColumns.Name) ?: return null,
+ uri = Uri.parse(get(ContactColumns.Uri) ?: return null),
phoneNumbers = get(ContactColumns.PhoneNumbers) ?: emptyList(),
emailAddresses = get(ContactColumns.EmailAddresses) ?: emptyList(),
postalAddresses = get(ContactColumns.PostalAddresses) ?: emptyList(),