diff --git a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt index 42c66aa29d..23c748d6da 100644 --- a/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt +++ b/features/announcement/api/src/main/kotlin/io/element/android/features/announcement/api/AnnouncementService.kt @@ -12,15 +12,34 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import kotlinx.coroutines.flow.Flow +/** + * Queues the full-screen announcements shown to the user once, such as the notice about a new feature. + * + * A dismissed announcement is remembered so it is not shown again. + */ interface AnnouncementService { + /** + * Queues an announcement, unless the user has already dismissed it. + * + * @param announcement the announcement to show. + */ suspend fun showAnnouncement(announcement: Announcement) + /** + * Records that the user dismissed an announcement, so it is not queued again. + * + * @param announcement the announcement that was dismissed. + */ suspend fun onAnnouncementDismissed(announcement: Announcement) + /** The announcements still waiting to be shown, in the order they should appear. */ fun announcementsToShowFlow(): Flow> /** * Use this composable to render the announcement UI in Fullscreen. + * Draws nothing while there is no announcement to show. + * + * @param modifier layout modifier for the container. */ @Composable fun Render( diff --git a/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleaner.kt b/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleaner.kt index b5a5396b16..48b464cb21 100644 --- a/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleaner.kt +++ b/features/cachecleaner/api/src/main/kotlin/io/element/android/features/cachecleaner/api/CacheCleaner.kt @@ -8,6 +8,9 @@ package io.element.android.features.cachecleaner.api +/** + * Removes the temporary files holding decrypted content, so plaintext media does not linger on disk. + */ interface CacheCleaner { /** * Clear the cache subdirs holding temporarily decrypted content (such as media and voice messages). diff --git a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallService.kt b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallService.kt index 25720596ba..20ff7ef4d8 100644 --- a/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallService.kt +++ b/features/call/api/src/main/kotlin/io/element/android/features/call/api/CurrentCallService.kt @@ -10,6 +10,9 @@ package io.element.android.features.call.api import kotlinx.coroutines.flow.StateFlow +/** + * Tracks the call the user is currently in on this device, so unrelated parts of the app can react to it. + */ interface CurrentCallService { /** * The current call state flow, which will be updated when the active call changes. diff --git a/features/contentscanner/api/src/main/kotlin/io/element/android/features/contentscanner/api/ContentScannerService.kt b/features/contentscanner/api/src/main/kotlin/io/element/android/features/contentscanner/api/ContentScannerService.kt index 68f7af28f1..3649768eb7 100644 --- a/features/contentscanner/api/src/main/kotlin/io/element/android/features/contentscanner/api/ContentScannerService.kt +++ b/features/contentscanner/api/src/main/kotlin/io/element/android/features/contentscanner/api/ContentScannerService.kt @@ -18,5 +18,11 @@ import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentVal * This will only process media when a [ContentScanner] is provided when building the client in Pro. */ fun interface ContentScannerService { + /** + * Starts scanning the given media, reporting the verdict of each one through [contentValidationState] rather than by returning. + * + * @param mediaSources the media to scan. + * @param contentValidationState where the outcome of each scan is published for the UI to observe. + */ fun scan(mediaSources: List, contentValidationState: ContentValidationState) } diff --git a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt index 271fdb2332..3128b8073a 100644 --- a/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt +++ b/features/createroom/api/src/main/kotlin/io/element/android/features/createroom/api/CreateRoomEntryPoint.kt @@ -15,9 +15,21 @@ import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.matrix.api.core.RoomId interface CreateRoomEntryPoint : FeatureEntryPoint { + /** + * Configures the create-room flow before it is started, since it serves rooms and spaces from the same screens. + */ interface Builder { + /** + * @param isSpace true to create a space rather than a room. + */ fun setIsSpace(isSpace: Boolean): Builder + + /** + * @param parentSpaceId the space the new room should be added to once created. + */ fun setParentSpace(parentSpaceId: RoomId): Builder + + /** Builds the node with the options configured so far. */ fun build(): Node } diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt index a435b53e48..7d23ad0ea1 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt @@ -15,11 +15,41 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.wellknown.api.ElementWellKnown import kotlinx.coroutines.flow.Flow +/** + * Exposes the restrictions and customisations an enterprise deployment can impose: allowed homeservers, branding, push gateways and more. + * + * On a standard build this reports permissive defaults, so callers do not need to branch on the build type themselves. + */ interface EnterpriseService { + /** Whether this build is an enterprise one; `false` for the public app. */ val isEnterpriseBuild: Boolean + + /** + * Whether the given session belongs to an enterprise deployment, which can be true even on a standard build. + * + * @param sessionId the session to check. + */ suspend fun isEnterpriseUser(sessionId: SessionId): Boolean + + /** + * Rewrites the authentication server URL when the deployment requires a different one from the one advertised. + * + * @param url the URL to rewrite. + * @param homeserver the homeserver the URL belongs to. + * @param urlContentFetcher used to read the deployment configuration; a client that is not authenticated yet also works. + */ suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher): String + + /** + * The homeservers the user is allowed to sign in to; an empty list, or one containing [ANY_ACCOUNT_PROVIDER], means no restriction. + */ fun defaultHomeserverList(): List + + /** + * Whether the user is allowed to sign in to a given homeserver, according to [defaultHomeserverList]. + * + * @param homeserverUrl the server the user is trying to use. + */ suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean /** @@ -29,17 +59,38 @@ interface EnterpriseService { */ suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) + /** + * The brand color to apply, or `null` when the default theme color should be used. + * + * @param sessionId the session to read the color of, or `null` for the color used when no session is active. + */ fun brandColorsFlow(sessionId: SessionId?): Flow + /** + * The full light and dark color palettes derived from the brand color. + * + * @param sessionId the session to read the colors of, or `null` for the colors used when no session is active. + */ fun semanticColorsFlow(sessionId: SessionId?): Flow + /** The push gateway to register with when using Firebase, or `null` to use the app default. */ fun firebasePushGateway(): String? + + /** The push gateway to register with when using UnifiedPush, or `null` to use the app default. */ fun unifiedPushDefaultPushGateway(): String? + /** + * Where bug reports should be submitted, which an enterprise deployment can redirect or disable entirely. + * + * @param sessionId the session to read the setting of, or `null` for the setting used when no session is active. + */ fun bugReportUrlFlow(sessionId: SessionId?): Flow /** * Gets Notification Channel to use for the noisy notifications of the provided session. + * Returns `null` when the session has no dedicated channel and the app-wide one should be used. + * + * @param sessionId the session whose channel is requested. */ fun getNoisyNotificationChannelId(sessionId: SessionId): String? @@ -50,6 +101,9 @@ interface EnterpriseService { /** * Gets the Element Server Suite (ESS) config endpoint URL for the given domain. + * Returns `null` when this build does not read its configuration from an ESS deployment. + * + * @param domain the server whose configuration endpoint is requested. */ fun essConfigEndpointUrl(domain: String): String? diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt index ee005a0b6b..84067ce56f 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/SessionEnterpriseService.kt @@ -8,11 +8,25 @@ package io.element.android.features.enterprise.api +/** + * The session-scoped counterpart of [EnterpriseService], answering the same kind of questions for the session it belongs to. + * + * [init] must have run before the other members return meaningful answers. + */ interface SessionEnterpriseService { + /** Whether Element Call may be used on this session, which a deployment can turn off. */ suspend fun isElementCallAvailable(): Boolean + + /** + * Rewrites the authentication server URL for this session; see [EnterpriseService.tweakMasUrl]. + * + * @param url the URL to rewrite. + */ suspend fun tweakMasUrl(url: String): String + /** Whether the homeserver forbids creating encrypted rooms, in which case the app must not offer the option. */ suspend fun isEncryptionDisabledByHomeserver(): Boolean + /** Loads the deployment configuration of this session; to be called once when the session starts. */ suspend fun init() } diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/remoteconfig/RemoteEnterpriseConfigProvider.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/remoteconfig/RemoteEnterpriseConfigProvider.kt index adbcc5b31a..4507b1a72a 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/remoteconfig/RemoteEnterpriseConfigProvider.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/remoteconfig/RemoteEnterpriseConfigProvider.kt @@ -15,9 +15,21 @@ import io.element.android.libraries.wellknown.api.WellknownRetrieverResult * Wrapper to fetch the remote enterprise configuration for a given [SessionId], either from a local cache or from a remote source. */ interface RemoteEnterpriseConfigProvider { + /** + * Returns the configuration of a session, served from the cache when possible. + * A stale entry is reported as outdated while a refresh runs in the background, so this rarely waits on the network. + * + * @param sessionId the session whose configuration is requested. + */ suspend fun get(sessionId: SessionId): WellknownRetrieverResult + /** + * Creates a provider, which needs a fetcher because the configuration is read over HTTP. + */ interface Factory { + /** + * @param urlContentFetcher used to perform the request; a client that is not authenticated yet also works. + */ fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider } } diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt index 046303ca4f..d8ab7c71c8 100644 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/SeenInvitesStore.kt @@ -11,6 +11,9 @@ package io.element.android.features.invite.api import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.coroutines.flow.Flow +/** + * Remembers which invitations the user has already looked at, so that only genuinely new ones are highlighted in the room list. + */ interface SeenInvitesStore { /** * Returns a flow of seen room IDs of invitation. @@ -32,7 +35,7 @@ interface SeenInvitesStore { suspend fun markAsUnSeen(roomId: RoomId) /** - * Delete the store. + * Delete the store, so every invitation counts as unseen again. */ suspend fun clear() } diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt index 2caaacab2b..5691b19ef7 100644 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt @@ -10,6 +10,9 @@ package io.element.android.features.invite.api.acceptdecline import io.element.android.features.invite.api.InviteData +/** + * Events sent to the accept/decline invite presenter, from whichever screen is showing the invitation. + */ interface AcceptDeclineInviteEvents { data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents data class DeclineInvite(val invite: InviteData, val blockUser: Boolean, val shouldConfirm: Boolean) : AcceptDeclineInviteEvents diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteView.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteView.kt index b6eec03a6f..6e9d78082b 100644 --- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteView.kt +++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteView.kt @@ -12,7 +12,20 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import io.element.android.libraries.matrix.api.core.RoomId +/** + * Renders the confirmation dialogs and error states of accepting or declining an invitation; it draws nothing until such an event is sent. + * + * Embedded by every screen that can act on an invitation, so the behaviour stays identical between the invite list and a room preview. + */ fun interface AcceptDeclineInviteView { + /** + * Draws whichever dialog the current state calls for. + * + * @param state the state produced by the accept/decline presenter. + * @param onAcceptInviteSuccess called once the room has been joined, so the host screen can navigate into it. + * @param onDeclineInviteSuccess called once the invitation has been declined. + * @param modifier layout modifier for the container. + */ @Composable fun Render( state: AcceptDeclineInviteState, diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt index 0422fac4f1..9570ab8152 100644 --- a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt +++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt @@ -8,6 +8,9 @@ package io.element.android.features.invitepeople.api +/** + * Events the invite people UI sends to its presenter through [InvitePeopleState.eventSink]. + */ interface InvitePeopleEvents { data object SendInvites : InvitePeopleEvents data object CloseSearch : InvitePeopleEvents diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeoplePresenter.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeoplePresenter.kt index 0be0798680..1863397e48 100644 --- a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeoplePresenter.kt +++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeoplePresenter.kt @@ -13,7 +13,14 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.JoinedRoom interface InvitePeoplePresenter : Presenter { + /** + * Creates a presenter for one invite flow. + */ interface Factory { + /** + * @param joinedRoom the room to invite into, or `null` when the room does not exist yet, as when inviting from a direct message. + * @param roomId the id of the room to invite into. + */ fun create( joinedRoom: JoinedRoom?, roomId: RoomId, diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleRenderer.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleRenderer.kt index 7bad81ab65..0bbaa45a2f 100644 --- a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleRenderer.kt +++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleRenderer.kt @@ -11,7 +11,16 @@ package io.element.android.features.invitepeople.api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +/** + * Renders the invite people UI, so that other screens can embed it without depending on its implementation. + */ interface InvitePeopleRenderer { + /** + * Draws the invite list and its search field. + * + * @param state the state produced by [InvitePeoplePresenter]. + * @param modifier layout modifier for the container. + */ @Composable fun Render( state: InvitePeopleState, diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt index d14042cff7..0863ec7212 100644 --- a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt +++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt @@ -11,10 +11,22 @@ package io.element.android.features.invitepeople.api import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId +/** + * State of the invite people UI, produced by an [InvitePeoplePresenter] and consumed by an [InvitePeopleRenderer]. + */ interface InvitePeopleState { + /** Whether the send button should be enabled, i.e. at least one user is selected and the user has the permission. */ val canInvite: Boolean + + /** Whether the search field currently has focus, which the host screen uses to adapt its own layout. */ val isSearchActive: Boolean + + /** Progress of the invite request, so the UI can show a loader and surface failures. */ val sendInvitesAction: AsyncAction + + /** Progress of turning a direct message into a room, which happens when inviting a third person into a DM. */ val createRoomFromDmAction: AsyncAction + + /** Where the UI sends its events. */ val eventSink: (InvitePeopleEvents) -> Unit } diff --git a/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/banner/KnockRequestsBannerRenderer.kt b/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/banner/KnockRequestsBannerRenderer.kt index 34061db335..e6be68b95a 100644 --- a/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/banner/KnockRequestsBannerRenderer.kt +++ b/features/knockrequests/api/src/main/kotlin/io/element/android/features/knockrequests/api/banner/KnockRequestsBannerRenderer.kt @@ -11,7 +11,16 @@ package io.element.android.features.knockrequests.api.banner import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +/** + * Renders the banner shown at the top of a room when people are waiting to join it. + */ interface KnockRequestsBannerRenderer { + /** + * Draws the banner, or nothing when there is no pending request or the user cannot moderate the room. + * + * @param modifier layout modifier for the container. + * @param onViewRequestsClick called when the user asks to see the full list of requests. + */ @Composable fun View(modifier: Modifier, onViewRequestsClick: () -> Unit) } diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt index 6d228ee887..f0c3eaf7bf 100644 --- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomEvent.kt @@ -10,6 +10,9 @@ package io.element.android.features.leaveroom.api import io.element.android.libraries.matrix.api.core.RoomId +/** + * Events sent to the leave room presenter through [LeaveRoomState.eventSink]. + */ interface LeaveRoomEvent { data class LeaveRoom(val roomId: RoomId, val needsConfirmation: Boolean) : LeaveRoomEvent } diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomRenderer.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomRenderer.kt index 852193d874..8472e098ec 100644 --- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomRenderer.kt +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomRenderer.kt @@ -12,7 +12,17 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import io.element.android.libraries.matrix.api.core.RoomId +/** + * Renders the confirmation dialogs of the leave room flow; it draws nothing until the host screen sends a leave event. + */ fun interface LeaveRoomRenderer { + /** + * Draws whichever dialog the current state calls for. + * + * @param state the state produced by the leave room presenter. + * @param onSelectNewOwners called when the user is the last owner of a room that still has other members, and must promote someone before leaving. + * @param modifier layout modifier for the container. + */ @Composable fun Render( state: LeaveRoomState, diff --git a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt index 92a5916858..b9150b59f0 100644 --- a/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt +++ b/features/leaveroom/api/src/main/kotlin/io/element/android/features/leaveroom/api/LeaveRoomState.kt @@ -10,7 +10,11 @@ package io.element.android.features.leaveroom.api import androidx.compose.runtime.Immutable +/** + * State of the leave room flow, which is only the confirmation dialogs: the screen it is hosted in owns the rest of the UI. + */ @Immutable interface LeaveRoomState { + /** Where the host screen sends its events to start or confirm leaving a room. */ val eventSink: (LeaveRoomEvent) -> Unit } diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocationService.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocationService.kt index 22049c9548..41c58c396f 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocationService.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocationService.kt @@ -8,6 +8,10 @@ package io.element.android.features.location.api +/** + * Tells whether the location features can be used at all, which depends on the build having a map provider configured. + */ interface LocationService { + /** Whether location sharing and map rendering are available; `false` disables the whole feature in the UI. */ fun isServiceAvailable(): Boolean } diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt index 9bff0ee754..212da08647 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt @@ -16,6 +16,17 @@ import io.element.android.libraries.wellknown.api.MapTilerConfig * Builds an URL for a 3rd party service provider static maps API. */ interface StaticMapUrlBuilder { + /** + * Builds the URL of a map image centred on the given coordinates. + * + * @param lat latitude of the centre of the map. + * @param lon longitude of the centre of the map. + * @param zoom zoom level to render at. + * @param darkMode whether to request the dark map style. + * @param width width of the image in density independent pixels. + * @param height height of the image in density independent pixels. + * @param density screen density, used to request a matching pixel size. + */ fun build( lat: Double, lon: Double, @@ -26,6 +37,7 @@ interface StaticMapUrlBuilder { density: Float, ): String + /** Whether a map provider is configured for this build; the URLs are unusable when it is not. */ fun isServiceAvailable(): Boolean } diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt index f0c946f3d0..4cfc8c4a2f 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt @@ -19,6 +19,12 @@ import io.element.android.libraries.wellknown.api.MapTilerConfig * Used for rendering dynamic maps. */ interface TileServerStyleUriBuilder { + /** + * Builds the style URI the map view loads its tiles from. + * + * @param customMapStyleUrl the style advertised by the homeserver, or `null` to use the one built into the app. + * @param darkMode whether to request the dark map style. + */ fun build( customMapTilerConfig: MapTilerConfig?, darkMode: Boolean, diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/live/ActiveLiveLocationShareManager.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/live/ActiveLiveLocationShareManager.kt index cd6b8731c1..64c28209ca 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/live/ActiveLiveLocationShareManager.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/live/ActiveLiveLocationShareManager.kt @@ -12,6 +12,9 @@ import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.coroutines.flow.StateFlow import kotlin.time.Duration +/** + * Owns the live location shares running on this device, across every room, and the foreground service that feeds them GPS updates. + */ interface ActiveLiveLocationShareManager { /** All rooms currently sharing live location on this device. */ val sharingRoomIds: StateFlow> @@ -27,6 +30,9 @@ interface ActiveLiveLocationShareManager { * Starts live location sharing in the given room. * Calls room.startLiveLocationShare() on the SDK, registers the share, * and starts the foreground GPS service if not already running. + * + * @param roomId the room to start sharing the location in. + * @param duration how long the share should last before it expires on its own. */ suspend fun startShare(roomId: RoomId, duration: Duration): Result @@ -34,6 +40,8 @@ interface ActiveLiveLocationShareManager { * Stops live location sharing in the given room. * Calls room.stopLiveLocationShare() on the SDK, removes the share, * and stops the foreground service if no shares remain. + * + * @param roomId the room to stop sharing the location in. */ suspend fun stopShare(roomId: RoomId): Result } diff --git a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenService.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenService.kt index bd644e69e9..59c148660e 100644 --- a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenService.kt +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenService.kt @@ -17,6 +17,9 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +/** + * Exposes the app lock state: whether a PIN or biometric lock is set up, whether it is mandatory, and whether the app is locked right now. + */ interface LockScreenService { /** * The current lock state of the app. diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginIntentResolver.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginIntentResolver.kt index 79cfc35fbd..b2b7378be4 100644 --- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginIntentResolver.kt +++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginIntentResolver.kt @@ -8,6 +8,13 @@ package io.element.android.features.login.api +/** + * Extracts the login parameters from a link, so that a deployment can hand out a URL that pre-fills the sign-in screen. + */ interface LoginIntentResolver { + /** + * @param uriString the link the app was opened with. + * @return the account provider and login hint to pre-fill, or `null` when the link is not a login link. + */ fun parse(uriString: String): LoginParams? } diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/accesscontrol/AccountProviderAccessControl.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/accesscontrol/AccountProviderAccessControl.kt index fc804c14ff..0c35c1655f 100644 --- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/accesscontrol/AccountProviderAccessControl.kt +++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/accesscontrol/AccountProviderAccessControl.kt @@ -8,6 +8,14 @@ package io.element.android.features.login.api.accesscontrol +/** + * Enforces the restriction an enterprise deployment can put on which account providers the user may sign in to. + */ interface AccountProviderAccessControl { + /** + * Whether sign-in to this provider is permitted; `true` on a build with no such restriction. + * + * @param accountProviderUrl the server the user is trying to use. + */ suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String): Boolean } diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutView.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutView.kt index 7841b85a5a..affd2be94d 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutView.kt +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutView.kt @@ -10,7 +10,17 @@ package io.element.android.features.logout.api.direct import androidx.compose.runtime.Composable +/** + * Renders the confirmation dialogs of logging out directly, in particular the warning shown when the user would lose their room keys. + * + * It draws nothing until the host screen sends a logout event. + */ fun interface DirectLogoutView { + /** + * Draws whichever dialog the current state calls for. + * + * @param state the state produced by the direct logout presenter. + */ @Composable fun Render(state: DirectLogoutState) } diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessageComposerContext.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessageComposerContext.kt index 534af8a5f9..12af1b32f2 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessageComposerContext.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessageComposerContext.kt @@ -17,5 +17,6 @@ import io.element.android.libraries.textcomposer.model.MessageComposerMode * the composer is in a thread, if it's editing a message, etc. */ interface MessageComposerContext { + /** What the composer is currently doing: writing a new message, editing one, replying, or composing in a thread. */ val composerMode: MessageComposerMode } diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt index 3eecd54f3e..dd49dd0465 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt @@ -50,7 +50,16 @@ interface MessagesEntryPoint : FeatureEntryPoint { callback: Callback, ): Node + /** + * Lets callers outside this feature drive an already created messages node, for navigation that has to happen after it is attached. + */ interface NodeProxy { + /** + * Opens a thread on top of the messages screen, suspending until the thread node is attached. + * + * @param threadId the thread to open. + * @param focusedEventId the event to scroll to within the thread, or `null` to open at the latest message. + */ suspend fun attachThread(threadId: ThreadId, focusedEventId: EventId?) } } diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/pinned/PinnedEventsTimelineProvider.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/pinned/PinnedEventsTimelineProvider.kt index 849b97479e..550373ed00 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/pinned/PinnedEventsTimelineProvider.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/pinned/PinnedEventsTimelineProvider.kt @@ -10,4 +10,9 @@ package io.element.android.features.messages.api.pinned import io.element.android.libraries.matrix.api.timeline.TimelineProvider +/** + * A [TimelineProvider] whose active timeline holds the pinned events of the room, rather than its live events. + * + * It exists as its own type so that it can be injected where only the pinned timeline is wanted. + */ interface PinnedEventsTimelineProvider : TimelineProvider diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/HtmlConverterProvider.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/HtmlConverterProvider.kt index f44c996cac..baeb37e417 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/HtmlConverterProvider.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/HtmlConverterProvider.kt @@ -11,9 +11,16 @@ package io.element.android.features.messages.api.timeline import androidx.compose.runtime.Composable import io.element.android.wysiwyg.utils.HtmlConverter +/** + * Provides the converter that turns message HTML into styled text. + * + * The converter depends on the current theme and typography, so [Update] must be composed before [provide] is called. + */ interface HtmlConverterProvider { + /** Rebuilds the converter from the current Compose theme; call this once, high in the composition of a screen that renders messages. */ @Composable fun Update() + /** Returns the converter built by the last [Update] call. */ fun provide(): HtmlConverter } diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerPresenter.kt index 848f9fe82d..832d029e40 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerPresenter.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerPresenter.kt @@ -12,7 +12,13 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.timeline.Timeline fun interface VoiceMessageComposerPresenter : Presenter { + /** + * Creates a presenter for one voice message composer. + */ interface Factory { + /** + * @param timelineMode the timeline the recording will be sent to, which matters for threads in particular. + */ fun create(timelineMode: Timeline.Mode): VoiceMessageComposerPresenter } } diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/EndPollAction.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/EndPollAction.kt index a46a2db781..b33a00c165 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/EndPollAction.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/EndPollAction.kt @@ -11,6 +11,13 @@ package io.element.android.features.poll.api.actions import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline +/** + * Closes a poll so no further answers are accepted, wrapping the timeline call so the analytics are captured in one place. + */ interface EndPollAction { + /** + * @param timeline the timeline the poll lives in. + * @param pollStartId the poll start event to close. + */ suspend fun execute(timeline: Timeline, pollStartId: EventId): Result } diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/SendPollResponseAction.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/SendPollResponseAction.kt index 00b60ed5ae..cbaace50fb 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/SendPollResponseAction.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/actions/SendPollResponseAction.kt @@ -11,7 +11,15 @@ package io.element.android.features.poll.api.actions import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline +/** + * Sends the current user's answer to a poll, wrapping the timeline call so the analytics are captured in one place. + */ interface SendPollResponseAction { + /** + * @param timeline the timeline the poll lives in. + * @param pollStartId the poll start event being answered. + * @param answerId the id of the chosen answer. + */ suspend fun execute( timeline: Timeline, pollStartId: EventId, diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFactory.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFactory.kt index c9bd31ea4f..f9b4a56efc 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFactory.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollContentStateFactory.kt @@ -12,7 +12,16 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.PollContent +/** + * Turns the poll content of a timeline event into the state the poll UI renders. + */ interface PollContentStateFactory { + /** + * Convenience overload that reads the editability and ownership from the timeline item itself. + * + * @param eventTimelineItem the timeline item carrying the poll. + * @param content the poll content of that item. + */ suspend fun create(eventTimelineItem: EventTimelineItem, content: PollContent): PollContentState { return create( eventId = eventTimelineItem.eventId, @@ -21,5 +30,12 @@ interface PollContentStateFactory { content = content, ) } + + /** + * @param eventId the poll start event, or `null` while the poll is still a local echo. + * @param isEditable whether the current user may still edit the poll. + * @param isOwn whether the poll was created by the current user. + * @param content the poll question, answers and votes. + */ suspend fun create(eventId: EventId?, isEditable: Boolean, isOwn: Boolean, content: PollContent): PollContentState } diff --git a/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/CacheService.kt b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/CacheService.kt index 82fcd1053f..9dc3eb1877 100644 --- a/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/CacheService.kt +++ b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/CacheService.kt @@ -11,6 +11,9 @@ package io.element.android.features.preferences.api import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.coroutines.flow.Flow +/** + * Announces that a session's cache has been cleared, since clearing it destroys the client and the app has to react. + */ interface CacheService { /** * A flow of [SessionId], can let the app to know when the diff --git a/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/ExtraDeveloperOptionsRenderer.kt b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/ExtraDeveloperOptionsRenderer.kt index 7d7eec9d8b..3acb2c9057 100644 --- a/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/ExtraDeveloperOptionsRenderer.kt +++ b/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/ExtraDeveloperOptionsRenderer.kt @@ -14,6 +14,11 @@ import androidx.compose.ui.Modifier * Component for rendering extra developer options in the preferences screen. */ interface ExtraDeveloperOptionsRenderer { + /** + * Draws the extra options. The standard build binds a no-op implementation, so only the builds that actually add options provide a real one. + * + * @param modifier layout modifier for the container. + */ @Composable fun Render(modifier: Modifier) } diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/RageshakeFeatureAvailability.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/RageshakeFeatureAvailability.kt index bc72e7021b..c4205c1575 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/RageshakeFeatureAvailability.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/RageshakeFeatureAvailability.kt @@ -10,6 +10,10 @@ package io.element.android.features.rageshake.api import kotlinx.coroutines.flow.Flow +/** + * Tells whether bug reporting is available, which an enterprise deployment can turn off. + */ fun interface RageshakeFeatureAvailability { + /** Whether the rageshake and bug report entry points should be offered to the user. */ fun isAvailable(): Flow } diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/LogFilesRemover.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/LogFilesRemover.kt index f8eee31675..a7b2961ab6 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/LogFilesRemover.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/logs/LogFilesRemover.kt @@ -10,6 +10,9 @@ package io.element.android.features.rageshake.api.logs import java.io.File +/** + * Deletes the log files the app writes, used to keep them from growing without bound and to clear them on request. + */ interface LogFilesRemover { /** * Perform the log files removal. diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt index 631a51a686..c1611f30b5 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt @@ -10,6 +10,11 @@ package io.element.android.features.rageshake.api.reporter import java.io.File +/** + * Uploads bug reports, including the log files the app has been writing. + * + * Progress and the outcome are reported through the listener passed to [sendBugReport] rather than by returning or throwing. + */ interface BugReporter { /** * Send a bug report. @@ -35,17 +40,21 @@ interface BugReporter { ) /** - * Provide the log directory. + * Provide the log directory, i.e. where the rotating log files are written. */ fun logDirectory(): File /** - * Set the current tracing log level. + * Set the current tracing log level, so it can be reported alongside the bug. + * + * @param logLevel the log level currently in effect. */ fun setCurrentTracingLogLevel(logLevel: String) /** * Save the logcat. + * + * @return the file the logcat was written to, or `null` when it could not be captured. */ fun saveLogCat(): File? } diff --git a/features/rolesandpermissions/api/src/main/kotlin/io/element/android/features/rolesandpermissions/api/ChangeRoomMemberRolesEntryPoint.kt b/features/rolesandpermissions/api/src/main/kotlin/io/element/android/features/rolesandpermissions/api/ChangeRoomMemberRolesEntryPoint.kt index 7cfdfefae0..4f823387f3 100644 --- a/features/rolesandpermissions/api/src/main/kotlin/io/element/android/features/rolesandpermissions/api/ChangeRoomMemberRolesEntryPoint.kt +++ b/features/rolesandpermissions/api/src/main/kotlin/io/element/android/features/rolesandpermissions/api/ChangeRoomMemberRolesEntryPoint.kt @@ -22,8 +22,18 @@ fun interface ChangeRoomMemberRolesEntryPoint : FeatureEntryPoint { listType: ChangeRoomMemberRolesListType, ): Node + /** + * Lets a caller outside this feature wait for the role change to finish, which is needed when leaving a room depends on it. + */ interface NodeProxy { + /** The room whose member roles are being changed. */ val roomId: RoomId + + /** + * Suspends until the user finishes or abandons the flow. + * + * @return true when roles were actually changed, false when the user backed out. + */ suspend fun waitForCompletion(): Boolean } } diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt index 2238ff41df..d452ff4e66 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt @@ -10,6 +10,9 @@ package io.element.android.features.roommembermoderation.api import io.element.android.libraries.matrix.api.user.MatrixUser +/** + * Events sent to the moderation presenter through [RoomMemberModerationState.eventSink]. + */ interface RoomMemberModerationEvents { data class ShowActionsForUser(val user: MatrixUser) : RoomMemberModerationEvents data class ProcessAction(val action: ModerationAction, val targetUser: MatrixUser) : RoomMemberModerationEvents diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt index efc87d8a40..7696f826a1 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationRenderer.kt @@ -12,7 +12,18 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import io.element.android.libraries.matrix.api.user.MatrixUser +/** + * Renders the moderation action sheet and its confirmation dialogs; it draws nothing until the host screen asks for a member's actions. + */ interface RoomMemberModerationRenderer { + /** + * Draws whichever sheet or dialog the current state calls for. + * + * @param state the state produced by the moderation presenter. + * @param onSelectAction called when the user picks an action, so the host screen can navigate if that action needs its own screen. + * @param onAvatarClick called when the user taps the member's avatar, or `null` to make it non-interactive. + * @param modifier layout modifier for the container. + */ @Composable fun Render( state: RoomMemberModerationState, diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt index c9ee958f6f..9227c77edf 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt @@ -10,9 +10,15 @@ package io.element.android.features.roommembermoderation.api import androidx.compose.runtime.Immutable +/** + * State of the member moderation flow, which is the action sheet and the confirmation dialogs shown for a room member. + */ @Immutable interface RoomMemberModerationState { + /** Which moderation actions the current user is allowed to perform, so the UI can hide or disable the rest. */ val permissions: RoomMemberModerationPermissions + + /** Where the host screen sends its events to open the actions of a member or run one of them. */ val eventSink: (RoomMemberModerationEvents) -> Unit } diff --git a/features/share/api/src/main/kotlin/io/element/android/features/share/api/OnSharedData.kt b/features/share/api/src/main/kotlin/io/element/android/features/share/api/OnSharedData.kt index d7985e8b06..f635cc0cce 100644 --- a/features/share/api/src/main/kotlin/io/element/android/features/share/api/OnSharedData.kt +++ b/features/share/api/src/main/kotlin/io/element/android/features/share/api/OnSharedData.kt @@ -11,5 +11,8 @@ package io.element.android.features.share.api * Post-processing to be done once a [ShareIntentData] has been consumed. */ fun interface OnSharedData { + /** + * @param data the shared content that has just been handled, so any temporary copy of it can be released. + */ operator fun invoke(data: ShareIntentData) } diff --git a/features/share/api/src/main/kotlin/io/element/android/features/share/api/ShareIntentHandler.kt b/features/share/api/src/main/kotlin/io/element/android/features/share/api/ShareIntentHandler.kt index d689c57398..d85ae9792a 100644 --- a/features/share/api/src/main/kotlin/io/element/android/features/share/api/ShareIntentHandler.kt +++ b/features/share/api/src/main/kotlin/io/element/android/features/share/api/ShareIntentHandler.kt @@ -9,10 +9,14 @@ package io.element.android.features.share.api import android.content.Intent +/** + * Recognises the intents another app uses to share content into this one, and resolves what was shared. + */ interface ShareIntentHandler { /** * This methods aims to handle incoming share intents and parse its data. * + * @param intent the intent the app was started with. * @return the [ShareIntentData] if it could be resolved, or null. */ fun handleIncomingShareIntent( diff --git a/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/StartDMAction.kt b/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/StartDMAction.kt index 6aa2ad5d98..2e5a92f18e 100644 --- a/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/StartDMAction.kt +++ b/features/startchat/api/src/main/kotlin/io/element/android/features/startchat/api/StartDMAction.kt @@ -13,6 +13,11 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.user.MatrixUser +/** + * Opens a direct message with a user, reusing the existing room when there is one. + * + * Progress is reported into the state passed in, so the caller only has to render it. + */ interface StartDMAction { /** * Try to find an existing DM with the given user, or create one if none exists. diff --git a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfilePresenterFactory.kt b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfilePresenterFactory.kt index 4bf68ae324..80a21505f5 100644 --- a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfilePresenterFactory.kt +++ b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfilePresenterFactory.kt @@ -11,6 +11,12 @@ package io.element.android.features.userprofile.api import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.core.UserId +/** + * Creates the user profile presenter, so that other features can show a profile without depending on this one's implementation. + */ fun interface UserProfilePresenterFactory { + /** + * @param userId the user whose profile is shown. + */ fun create(userId: UserId): Presenter } diff --git a/features/viewfolder/api/src/main/kotlin/io/element/android/features/viewfolder/api/TextFileViewer.kt b/features/viewfolder/api/src/main/kotlin/io/element/android/features/viewfolder/api/TextFileViewer.kt index 29a7c9f9e8..5abc27546d 100644 --- a/features/viewfolder/api/src/main/kotlin/io/element/android/features/viewfolder/api/TextFileViewer.kt +++ b/features/viewfolder/api/src/main/kotlin/io/element/android/features/viewfolder/api/TextFileViewer.kt @@ -12,7 +12,16 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import kotlinx.collections.immutable.ImmutableList +/** + * Renders the contents of a text file, used by the log viewer in the developer options. + */ fun interface TextFileViewer { + /** + * Draws the file, one row per line. + * + * @param lines the already-read contents of the file. + * @param modifier layout modifier for the container. + */ @Composable fun Render( lines: ImmutableList, diff --git a/libraries/audio/api/src/main/kotlin/io/element/android/libraries/audio/api/AudioFocus.kt b/libraries/audio/api/src/main/kotlin/io/element/android/libraries/audio/api/AudioFocus.kt index aeccaa9b8a..de2e5844d5 100644 --- a/libraries/audio/api/src/main/kotlin/io/element/android/libraries/audio/api/AudioFocus.kt +++ b/libraries/audio/api/src/main/kotlin/io/element/android/libraries/audio/api/AudioFocus.kt @@ -15,6 +15,9 @@ enum class AudioFocusRequester { MediaViewer, } +/** + * Claims and releases system audio focus, so that the app's own players do not talk over each other or over other apps. + */ interface AudioFocus { /** * Request audio focus for the given requester. diff --git a/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheStore.kt b/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheStore.kt index 5df446f688..bbfb7d3405 100644 --- a/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheStore.kt +++ b/libraries/cachestore/api/src/main/kotlin/io/element/android/libraries/cachestore/api/CacheStore.kt @@ -7,9 +7,33 @@ package io.element.android.libraries.cachestore.api +/** + * A generic on-disk key/value cache, where each entry records when it was written so callers can decide for themselves when it is stale. + */ interface CacheStore { + /** + * Stores an entry, replacing any previous one under the same key. + * + * @param key the entry to write. + * @param data the value along with the time it was obtained. + */ suspend fun storeData(key: String, data: CacheData) + + /** + * Reads an entry without applying any expiry of its own; judging staleness is up to the caller. + * + * @param key the entry to read. + * @return the value and its timestamp, or `null` when nothing is stored under that key. + */ suspend fun getData(key: String): CacheData? + + /** + * Removes one entry. + * + * @param key the entry to delete. + */ suspend fun deleteData(key: String) + + /** Empties the whole cache. */ suspend fun deleteAll() } diff --git a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionDecryptionService.kt b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionDecryptionService.kt index fc882bc54b..1e2866a20f 100644 --- a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionDecryptionService.kt +++ b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/EncryptionDecryptionService.kt @@ -15,8 +15,35 @@ import javax.crypto.SecretKey * Simple service to provide encryption and decryption operations. */ interface EncryptionDecryptionService { + /** + * Creates a cipher ready to encrypt, for the callers that need the cipher itself, such as to bind it to a biometric prompt. + * + * @param key the key to encrypt with. + */ fun createEncryptionCipher(key: SecretKey): Cipher + + /** + * Creates a cipher ready to decrypt, for the callers that need the cipher itself. + * + * @param key the key to decrypt with. + * @param initializationVector the vector produced when the data was encrypted. + */ fun createDecryptionCipher(key: SecretKey, initializationVector: ByteArray): Cipher + + /** + * Encrypts data in one call. + * + * @param key the key to encrypt with. + * @param input the plaintext bytes. + * @return the ciphertext along with the initialization vector needed to decrypt it. + */ fun encrypt(key: SecretKey, input: ByteArray): EncryptionResult + + /** + * Decrypts data in one call. + * + * @param key the key the data was encrypted with. + * @param encryptionResult the ciphertext and its initialization vector, as returned by [encrypt]. + */ fun decrypt(key: SecretKey, encryptionResult: EncryptionResult): ByteArray } diff --git a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyRepository.kt b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyRepository.kt index b210664d9f..c68f3170b9 100644 --- a/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyRepository.kt +++ b/libraries/cryptography/api/src/main/kotlin/io/element/android/libraries/cryptography/api/SecretKeyRepository.kt @@ -16,10 +16,17 @@ import javax.crypto.SecretKey * Implementation should be able to store the generated key securely. */ interface SecretKeyRepository { + /** + * Whether a key exists for the given alias, re-emitted when it is created or deleted. + * + * @param alias the alias to watch. + */ fun hasKey(alias: String): Flow /** * Get or create a secret key for a given alias. + * Note that [requiresUserAuthentication] only applies when the key is created: an existing key keeps the protection it was created with. + * * @param alias the alias to use * @param requiresUserAuthentication true if the key should be protected by user authentication */ diff --git a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DateFormatter.kt b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DateFormatter.kt index 1f5c88f008..84cbf5efdd 100644 --- a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DateFormatter.kt +++ b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DateFormatter.kt @@ -8,7 +8,17 @@ package io.element.android.libraries.dateformatter.api +/** + * Formats timestamps for display, in the user's locale and time zone. + */ interface DateFormatter { + /** + * Formats a timestamp according to [mode]. + * + * @param timestamp milliseconds since the epoch; a `null` value yields an empty string, so callers need no null check of their own. + * @param mode how much of the date and time to include; see [DateFormatterMode] for examples of each. + * @param useRelative true to prefer wording relative to today, such as "Today" or "This month", where the mode allows it. + */ fun format( timestamp: Long?, mode: DateFormatterMode = DateFormatterMode.Full, diff --git a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DurationFormatter.kt b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DurationFormatter.kt index ac297d10e9..712355c423 100644 --- a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DurationFormatter.kt +++ b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DurationFormatter.kt @@ -21,6 +21,11 @@ import kotlin.time.Duration * - 30 seconds → "30 seconds" */ interface DurationFormatter { + /** + * Formats the duration using its largest appropriate unit, rounding rather than listing several units. + * + * @param duration the duration to describe. + */ fun format(duration: Duration): String } diff --git a/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeepLinkCreator.kt b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeepLinkCreator.kt index 2b46b15e46..938939c6f0 100644 --- a/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeepLinkCreator.kt +++ b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeepLinkCreator.kt @@ -13,6 +13,17 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId +/** + * Builds the app's internal deep links, used to point a notification or a shortcut at a precise place in the app. + */ fun interface DeepLinkCreator { + /** + * Builds a link to the most precise target the arguments allow, ignoring the deeper ones once an outer one is `null`. + * + * @param sessionId the session to open. + * @param roomId the room to open, or `null` to stop at the room list. + * @param threadId the thread to open within that room, or `null` for the main timeline. + * @param eventId the event to focus on, or `null` to open at the latest message. + */ fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?, eventId: EventId?): String } diff --git a/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeeplinkParser.kt b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeeplinkParser.kt index f0f8640a2d..7a711d8a3a 100644 --- a/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeeplinkParser.kt +++ b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/DeeplinkParser.kt @@ -10,6 +10,13 @@ package io.element.android.libraries.deeplink.api import android.content.Intent +/** + * Extracts the app's own deep link data from an incoming intent; see [DeepLinkCreator] for the other direction. + */ fun interface DeeplinkParser { + /** + * @param intent the intent the app was started or resumed with. + * @return the session, room and event to open, or `null` when the intent is not one of the app's deep links. + */ fun getFromIntent(intent: Intent): DeeplinkData? } diff --git a/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/usecase/InviteFriendsUseCase.kt b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/usecase/InviteFriendsUseCase.kt index 64287f918c..2c4406a482 100644 --- a/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/usecase/InviteFriendsUseCase.kt +++ b/libraries/deeplink/api/src/main/kotlin/io/element/android/libraries/deeplink/api/usecase/InviteFriendsUseCase.kt @@ -10,6 +10,12 @@ package io.element.android.libraries.deeplink.api.usecase import android.app.Activity +/** + * Opens the system share sheet with an invitation to install the app. + */ fun interface InviteFriendsUseCase { + /** + * @param activity the activity the share sheet is shown from. + */ fun execute(activity: Activity) } diff --git a/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerPresenter.kt b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerPresenter.kt index a20b53a480..ac83b49533 100644 --- a/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerPresenter.kt +++ b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerPresenter.kt @@ -11,7 +11,14 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.emoji.api.recentemojis.GetRecentEmojis fun interface EmojiPickerPresenter : Presenter { + /** + * Creates a presenter for one picker instance. + */ fun interface Factory { + /** + * @param getRecentEmojis how the picker sources its recent tab; pass [io.element.android.libraries.emoji.api.recentemojis.EmptyGetRecentEmojis] + * for a picker that should have no recent tab at all. + */ fun create(getRecentEmojis: GetRecentEmojis): EmojiPickerPresenter } } diff --git a/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerRenderer.kt b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerRenderer.kt index b3784b3c42..b273f41dae 100644 --- a/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerRenderer.kt +++ b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/picker/EmojiPickerRenderer.kt @@ -13,9 +13,16 @@ import androidx.compose.ui.Modifier import io.element.android.emojibasebindings.Emoji import kotlinx.collections.immutable.ImmutableSet +/** + * Renders the emoji picker, so that callers can embed it without depending on its implementation. + * + * See [NoOpEmojiPickerRenderer] for a renderer that draws nothing, used where the picker is disabled. + */ @Immutable interface EmojiPickerRenderer { /** + * Draws the picker. + * * @param state opaque state produced by [EmojiPickerPresenter.present]. * @param onSelectEmoji invoked when the user taps an emoji (including a skin-tone variant). * @param selectedEmojis emojis to visually mark as already selected. diff --git a/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/AddRecentEmoji.kt b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/AddRecentEmoji.kt index 5865568511..af2a1dfd7d 100644 --- a/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/AddRecentEmoji.kt +++ b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/AddRecentEmoji.kt @@ -8,6 +8,12 @@ package io.element.android.libraries.emoji.api.recentemojis +/** + * Records that an emoji was just used, so it surfaces in the picker's recent list; see [GetRecentEmojis] for the read side. + */ fun interface AddRecentEmoji { + /** + * @param emoji the emoji that was just used, as its unicode representation. + */ suspend operator fun invoke(emoji: String): Result } diff --git a/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/GetRecentEmojis.kt b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/GetRecentEmojis.kt index cf6015d799..276b0e7eba 100644 --- a/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/GetRecentEmojis.kt +++ b/libraries/emoji/api/src/main/kotlin/io/element/android/libraries/emoji/api/recentemojis/GetRecentEmojis.kt @@ -15,6 +15,7 @@ import kotlinx.collections.immutable.persistentListOf * Returns the list of recently used emojis for reactions. */ fun interface GetRecentEmojis { + /** Returns the recently used emojis, most recent first; see [EmptyGetRecentEmojis] for a picker with no recent tab. */ suspend operator fun invoke(): Result> } diff --git a/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/PinnedMessagesBannerFormatter.kt b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/PinnedMessagesBannerFormatter.kt index 67994f18b9..986e5dfe32 100644 --- a/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/PinnedMessagesBannerFormatter.kt +++ b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/PinnedMessagesBannerFormatter.kt @@ -10,6 +10,12 @@ package io.element.android.libraries.eventformatter.api import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem +/** + * Formats the short summary of a pinned event shown in the banner at the top of a room. + */ interface PinnedMessagesBannerFormatter { + /** + * @param event the pinned event to describe. + */ fun format(event: EventTimelineItem): CharSequence } diff --git a/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/RoomLatestEventFormatter.kt b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/RoomLatestEventFormatter.kt index abb5cc929d..7ddc65f57b 100644 --- a/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/RoomLatestEventFormatter.kt +++ b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/RoomLatestEventFormatter.kt @@ -10,7 +10,25 @@ package io.element.android.libraries.eventformatter.api import io.element.android.libraries.matrix.api.roomlist.LatestEventValue +/** + * Formats the one-line preview of a room's latest event, as shown in the room list. + */ interface RoomLatestEventFormatter { + /** + * Formats an event that is still local, i.e. queued or being sent. + * + * @param latestEvent the local event to describe. + * @param isDmRoom whether the room is a direct message, which lets the sender name be left out. + * @return the preview text, or `null` when this kind of event should not be previewed. + */ fun format(latestEvent: LatestEventValue.Local, isDmRoom: Boolean): CharSequence? + + /** + * Formats an event that has been received from the server. + * + * @param latestEvent the remote event to describe. + * @param isDmRoom whether the room is a direct message, which lets the sender name be left out. + * @return the preview text, or `null` when this kind of event should not be previewed. + */ fun format(latestEvent: LatestEventValue.Remote, isDmRoom: Boolean): CharSequence? } diff --git a/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/TimelineEventFormatter.kt b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/TimelineEventFormatter.kt index be601fb82a..67d263f168 100644 --- a/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/TimelineEventFormatter.kt +++ b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/TimelineEventFormatter.kt @@ -13,7 +13,16 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName +/** + * Turns a state or membership event into the human readable sentence shown in the timeline, such as "Alice joined the room". + */ interface TimelineEventFormatter { + /** + * Convenience overload that reads the sender and ownership from the timeline item itself. + * + * @param event the timeline item to describe. + * @return the sentence to display, or `null` when this kind of event is not rendered as text. + */ fun format(event: EventTimelineItem): CharSequence? { return format( content = event.content, @@ -22,5 +31,13 @@ interface TimelineEventFormatter { senderDisambiguatedDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender), ) } + + /** + * @param content the event content to describe. + * @param isOutgoing whether the event was sent by the current user, which selects the first-person wording. + * @param sender the user who sent the event. + * @param senderDisambiguatedDisplayName the sender's display name, already suffixed with their id when it is ambiguous in the room. + * @return the sentence to display, or `null` when this kind of event is not rendered as text. + */ fun format(content: EventContent, isOutgoing: Boolean, sender: UserId, senderDisambiguatedDisplayName: String): CharSequence? } diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/Feature.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/Feature.kt index 395fd04c6c..4fd8706d89 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/Feature.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/Feature.kt @@ -10,6 +10,9 @@ package io.element.android.libraries.featureflag.api import io.element.android.libraries.core.meta.BuildMeta +/** + * Describes one feature flag: how it is identified, how it is presented in the developer options, and what it defaults to. + */ interface Feature { /** * Unique key to identify the feature. diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt index 6a8cb2fd9d..3dc791317f 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlagService.kt @@ -11,6 +11,9 @@ package io.element.android.libraries.featureflag.api import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +/** + * Reads and, where a mutable provider is registered, changes the state of the feature flags. + */ interface FeatureFlagService { /** * @param feature the feature to check for diff --git a/libraries/indicator/api/src/main/kotlin/io/element/android/libraries/indicator/api/IndicatorService.kt b/libraries/indicator/api/src/main/kotlin/io/element/android/libraries/indicator/api/IndicatorService.kt index 0409a0f846..d2fde82d8a 100644 --- a/libraries/indicator/api/src/main/kotlin/io/element/android/libraries/indicator/api/IndicatorService.kt +++ b/libraries/indicator/api/src/main/kotlin/io/element/android/libraries/indicator/api/IndicatorService.kt @@ -15,9 +15,11 @@ import androidx.compose.runtime.State * A set of State to observe to display or not the indicators in the UI. */ interface IndicatorService { + /** Whether to show the red dot on the room list top bar, which aggregates the reasons the user should open the settings. */ @Composable fun showRoomListTopBarIndicator(): State + /** Whether to show the red dot on the chat backup setting, i.e. when recovery needs the user's attention. */ @Composable fun showSettingChatBackupIndicator(): State } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/HomeserverCapabilitiesProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/HomeserverCapabilitiesProvider.kt index e914656f5f..fe05b88dc4 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/HomeserverCapabilitiesProvider.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/HomeserverCapabilitiesProvider.kt @@ -10,6 +10,8 @@ package io.element.android.libraries.matrix.api /** * Provides information about the capabilities of the homeserver. * + * The getters read a snapshot held by the SDK rather than querying the server on each call, so [refresh] must be used to pick up server-side changes. + * * Spec: https://spec.matrix.org/latest/client-server-api/#capabilities-negotiation */ interface HomeserverCapabilitiesProvider { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 26d7d02016..b83d96af96 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -54,47 +54,189 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import java.util.Optional +/** + * Entry point for everything scoped to a single logged in session: the sub-services, room access, and account level operations. + * + * One instance exists per session, created on login or session restore and torn down by [logout], [clearCache] or account deactivation. + * Suspending members here report failures through a [Result] instead of throwing, and run their work off the main thread. + */ interface MatrixClient : UrlContentFetcher { + /** Identifier of the logged in user; a [SessionId] and a [UserId] are the same value. */ val sessionId: SessionId + + /** Identifier of this device, as assigned by the homeserver when the session was created. */ val deviceId: DeviceId /** Base URL of the homeserver this client is connected to. */ val homeserverUrl: String + + /** Directories holding this session's databases, caches and downloaded media. */ val sessionPaths: SessionPaths + + /** + * Display name and avatar of the logged in user. + * Seeded from the local session store so a value is available before the first network fetch, then kept up to date by the SDK. + */ val userProfile: StateFlow + + /** Gives access to the list of rooms the user is a member of, or has been invited to. */ val roomListService: RoomListService + + /** Gives access to the spaces the user belongs to and their hierarchy. */ val spaceService: SpaceService + + /** Controls the sync loop of this session and exposes its state. */ val syncService: SyncService + + /** Handles verifying this session against the user's other devices or their recovery key. */ val sessionVerificationService: SessionVerificationService + + /** Manages the push gateways (pushers) registered on the homeserver for this session. */ val pushersService: PushersService + + /** Resolves incoming push notifications into renderable events. */ val notificationService: NotificationService + + /** Reads and writes the push notification rules of the account. */ val notificationSettingsService: NotificationSettingsService + + /** Gives access to end-to-end encryption state, recovery and key backup. */ val encryptionService: EncryptionService + + /** Gives access to the public room directory of the homeserver. */ val roomDirectoryService: RoomDirectoryService /** Whether this live client was built with a message search index attached. */ val isMessageSearchAvailable: Boolean + + /** Searches messages in the local index; only usable when [isMessageSearchAvailable] is `true`. */ val messageSearchService: MessageSearchService + + /** Reads and writes the account settings controlling whether media previews and avatars are shown. */ val mediaPreviewService: MediaPreviewService + + /** Downloads media content, thumbnails and files from the homeserver, caching them on disk. */ val matrixMediaLoader: MatrixMediaLoader + + /** + * Coroutine scope tied to the lifetime of this session, to be used for work that must stop when the session goes away. + * It is cancelled when the session is logged out or otherwise destroyed, so never use it for work that must outlive the session. + */ val sessionCoroutineScope: CoroutineScope + + /** The users the logged in user has ignored; starts empty and is updated by the SDK, including with an initial value. */ val ignoredUsersFlow: StateFlow> + + /** Notifies of membership changes the current user makes, so that callers can react to leaving or joining a room. */ val roomMembershipObserver: RoomMembershipObserver + + /** Emits updates about the live location beacons published by the current user. */ val ownBeaconInfoUpdates: Flow + + /** Scans media content before it is displayed, or `null` when no content scanner is configured for this session. */ val contentScanner: ContentScanner? + + /** + * Returns the room with the given id if the user has joined it, along with its live [Timeline]. + * A new instance is created on each call and the caller owns it, so it must be closed once no longer needed. + * + * @param roomId the id of the room to open. + * @return the room, or `null` if it is unknown to the client or the user is not joined. + */ suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom? + + /** + * Returns the room with the given id whatever the current membership is, without building a timeline. + * As with [getJoinedRoom], a new instance is created on each call and must be closed by the caller. + * + * @param roomId the id of the room to open. + * @return the room, or `null` if it is unknown to the client. + */ suspend fun getRoom(roomId: RoomId): BaseRoom? + + /** + * Looks for an existing direct message room shared with [userId]. + * + * @param userId the other member of the direct message room. + * @return the room id, or `null` if no direct message room exists with this user. + */ suspend fun findDM(userId: UserId): Result + + /** + * Returns the ids of every room the user has joined, as currently known by the client. + * Invited, knocked and left rooms are excluded. + */ suspend fun getJoinedRoomIds(): Result> + + /** + * Adds [userId] to the ignore list of the account, which hides their events and makes [ignoredUsersFlow] emit. + * + * @param userId the user to ignore. + */ suspend fun ignoreUser(userId: UserId): Result + + /** + * Removes [userId] from the ignore list of the account, which makes [ignoredUsersFlow] emit. + * + * @param userId the user to stop ignoring. + */ suspend fun unignoreUser(userId: UserId): Result + + /** + * Creates a room, applying the app's default power levels on top of [createRoomParams]. + * Waits a short while for the new room to come back through sync, but a timeout there is only logged: the result is still a success. + * + * @param createRoomParams the name, topic, preset, invitees and other settings of the room to create. + */ suspend fun createRoom(createRoomParams: CreateRoomParameters): Result + + /** + * Creates a direct message room with [userId], as a private, invite-only room with the user already invited. + * This does not check whether such a room already exists; use [findDM] first if you want to reuse one. + * + * @param userId the user to start the direct message with. + * @param isEncrypted whether the room should have end-to-end encryption enabled. + */ suspend fun createDM(userId: UserId, isEncrypted: Boolean): Result + + /** + * Fetches the current profile of any user from the homeserver, bypassing the local cache. + * + * @param userId the user whose profile is requested. + */ suspend fun getProfile(userId: UserId): Result + + /** + * Searches the homeserver's user directory. + * + * @param searchTerm the text to look for in user ids and display names. + * @param limit the maximum number of results to return. + */ suspend fun searchUsers(searchTerm: String, limit: Long): Result + + /** + * Changes the display name of the logged in user on the homeserver. + * + * @param displayName the new display name. + */ suspend fun setDisplayName(displayName: String): Result + + /** + * Uploads [data] as the new avatar of the logged in user. + * + * @param mimeType the MIME type of the image, for instance `image/jpeg`. + * @param data the raw bytes of the image. + */ suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result + + /** Removes the avatar of the logged in user. */ suspend fun removeAvatar(): Result + + /** + * Sets the `m.status` profile field of the logged in user (MSC4426), refreshing [userProfile] if the server does not push the change itself. + * + * @param status the status to publish. + */ suspend fun setUserStatus(status: UserStatus): Result /** Clears both m.status and m.call profile fields (maps to DELETE on the profile endpoint per MSC4426). */ @@ -103,16 +245,52 @@ interface MatrixClient : UrlContentFetcher { /** Whether the homeserver advertises support for user status (MSC4426). */ suspend fun isUserStatusSupported(): Result - /** Enable or disable automatically setting the user's status to "in a call" (m.call) while in a call. */ + /** + * Enable or disable automatically setting the user's status to "in a call" (m.call) while in a call. + * + * @param enabled whether the status should be maintained automatically. + */ fun enableAutomaticCallStatus(enabled: Boolean) + + /** + * Joins a room the client already knows about, then waits briefly for the new membership to arrive through sync. + * The returned [RoomInfo] is `null` when that wait times out, which is not treated as a failure: the join itself succeeded. + * + * @param roomId the id of the room to join. + */ suspend fun joinRoom(roomId: RoomId): Result + + /** + * Joins a room by id or by alias, resolving it through [serverNames] when the client does not know it yet. + * As with [joinRoom], the returned [RoomInfo] is `null` if the membership did not arrive through sync in time. + * + * @param roomIdOrAlias the id or the alias of the room to join. + * @param serverNames servers to ask about the room, used when it cannot be resolved locally. + */ suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result + + /** + * Sends a request to join a room whose join rule is knock, then waits briefly for the knocked membership to arrive through sync. + * As with [joinRoom], the returned [RoomInfo] is `null` if that wait times out. + * + * @param roomIdOrAlias the id or the alias of the room to knock on. + * @param message the reason shown to the room moderators; may be empty. + * @param serverNames servers to ask about the room, used when it cannot be resolved locally. + */ suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List): Result + + /** + * Returns the number of bytes this session occupies on disk that [clearCache] would be able to reclaim. + * This covers the cache directory, the state database and the message search index, but not the crypto database. + */ suspend fun getCacheSize(): Long + + /** Returns the size of each individual store the SDK maintains for this session, as reported by the SDK. */ suspend fun getDatabaseSizes(): Result /** - * Will close the client and delete the cache data. + * Deletes the cached data of this session and destroys the client. + * The instance is unusable afterwards, so callers are expected to restart the session, unlike with [logout] the session itself is kept. */ suspend fun clearCache() @@ -125,29 +303,56 @@ interface MatrixClient : UrlContentFetcher { suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean) /** - * Retrieve the user profile, will also eventually emit a new value to [userProfile]. + * Retrieve the profile of the logged in user, emitting it to [userProfile] and persisting it in the local session store on success. */ suspend fun getUserProfile(): Result + + /** + * Returns the web URL where the user can manage their account on the authentication provider, or `null` if the homeserver exposes none. + * + * @param action an optional section of the account management page to open directly, such as changing the password. + */ suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result + + /** + * Uploads arbitrary content to the homeserver's media repository. + * + * @param mimeType the MIME type of the content being uploaded. + * @param data the raw bytes to upload. + * @return the `mxc://` URI the content is now available at. + */ suspend fun uploadMedia(mimeType: String, data: ByteArray): Result /** * Get a room info flow for a given room ID. - * The flow will emit a new value whenever the room info is updated. - * The flow will emit Optional.empty item if the room is not found. + * The flow will emit a new value whenever the room info is updated, skipping duplicates, and an empty [Optional] if the room is not found. + * + * @param roomId the id of the room to observe. */ fun getRoomInfoFlow(roomId: RoomId): Flow> + /** + * Whether [userId] is the logged in user, `false` for a `null` value. + * + * @param userId the user id to compare against this session's user. + */ fun isMe(userId: UserId?) = userId == sessionId + /** + * Adds a room to the account's list of recently visited rooms, which backs the room suggestions when sharing or forwarding. + * + * @param roomId the id of the room that was just visited. + */ suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result + + /** Returns the recently visited rooms of the account, most recent first. */ suspend fun getRecentlyVisitedRooms(): Result> /** * Resolves the given room alias to a roomID (and a list of servers), if possible. - * @param roomAlias the room alias to resolve - * @return the resolved room alias if any, an empty result if not found,or an error if the resolution failed. * + * @param roomAlias the room alias to resolve. + * @return the resolved room alias if any, an empty result if not found, or an error if the resolution failed. */ suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result> @@ -158,6 +363,8 @@ interface MatrixClient : UrlContentFetcher { * event with it failed (e.g. sending an event via the Timeline), * so it's required to manually re-enable it as soon as * connectivity is back on the device. + * + * @param enabled whether the send queues of every room should be enabled. */ suspend fun setAllSendQueuesEnabled(enabled: Boolean) @@ -175,6 +382,9 @@ interface MatrixClient : UrlContentFetcher { /** * Get a room preview for a given room ID or alias. This is especially useful for rooms that the user is not a member of, or hasn't joined yet. + * + * @param roomIdOrAlias the id or the alias of the room to preview. + * @param serverNames servers to ask about the room, used when it cannot be resolved locally. */ suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result @@ -183,11 +393,21 @@ interface MatrixClient : UrlContentFetcher { */ suspend fun currentSlidingSyncVersion(): Result + /** Whether this account can be deactivated from the app, which depends on the authentication method used by the homeserver. */ fun canDeactivateAccount(): Boolean + + /** + * Deactivates the account on the homeserver, then destroys this client and deletes every local trace of the session. + * The password is only sent if the homeserver asks for re-authentication, and on failure the session is left untouched. + * + * @param password the account password, required to confirm the deactivation. + * @param eraseData whether the homeserver should also erase the messages the user sent. + */ suspend fun deactivateAccount(password: String, eraseData: Boolean): Result /** - * Check if the user can report a room. + * Whether the homeserver supports reporting a whole room, rather than individual events. + * Returns `false` if the capability cannot be determined. */ suspend fun canReportRoom(): Boolean @@ -208,16 +428,23 @@ interface MatrixClient : UrlContentFetcher { /** * Adds an emoji to the list of recent emoji reactions for this account. + * + * @param emoji the emoji that was just used. */ suspend fun addRecentEmoji(emoji: String): Result /** * Returns the raw JSON content of the global account data event of type [eventType], or null if it is not set. + * + * @param eventType the Matrix event type of the account data to read, for instance `m.direct`. */ suspend fun getAccountData(eventType: String): Result /** * Sets the global account data event of type [eventType] to the raw JSON [content]. + * + * @param eventType the Matrix event type of the account data to write. + * @param content the new content, as a raw JSON object. */ suspend fun setAccountData(eventType: String, content: String): Result @@ -226,6 +453,9 @@ interface MatrixClient : UrlContentFetcher { * * This method should be used with caution as providing the [eventId] ourselves can result in incorrect read receipts. * Use [Timeline.markAsRead] instead when possible. + * + * @param roomId the id of the room to mark as read. + * @param eventId the event up to which the room is considered read. */ suspend fun markRoomAsFullyRead(roomId: RoomId, eventId: EventId): Result @@ -252,6 +482,7 @@ interface MatrixClient : UrlContentFetcher { /** * Performs a database optimization that should flush cached data and improve performance. + * A periodic background job already calls this for every session, so callers rarely need to trigger it by hand. */ suspend fun performDatabaseVacuum(): Result @@ -260,6 +491,7 @@ interface MatrixClient : UrlContentFetcher { */ suspend fun resetWellKnownConfig(): Result + /** Returns a provider for the capabilities the homeserver advertises, such as whether the display name can be changed. */ fun homeserverCapabilities(): HomeserverCapabilitiesProvider } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt index 54f70845fa..08073d0cc0 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt @@ -10,18 +10,26 @@ package io.element.android.libraries.matrix.api import io.element.android.libraries.matrix.api.core.SessionId +/** + * In-memory cache of the [MatrixClient] of every logged in session. + * + * Prefer injecting the [MatrixClient] directly: this provider is meant for the entry points that only know a [SessionId], such as push handling and workers. + */ interface MatrixClientProvider { /** - * Can be used to get or restore a MatrixClient with the given [SessionId]. - * If a [MatrixClient] is already in memory, it'll return it. Otherwise it'll try to restore one. - * Most of the time you want to use injected constructor instead of retrieving a MatrixClient with this provider. + * Returns the cached [MatrixClient] for [sessionId], restoring the session from storage and starting its sync if it is not in memory yet. + * Concurrent calls are serialised, so only one restore happens per session. + * + * @param sessionId the [SessionId] of the session to get or restore. + * @return the client, or a failure if the session could not be restored. */ suspend fun getOrRestore(sessionId: SessionId): Result /** - * Can be used to retrieve an existing [MatrixClient] with the given [SessionId]. - * @param sessionId the [SessionId] of the [MatrixClient] to retrieve. - * @return the [MatrixClient] if it exists. + * Returns the [MatrixClient] for [sessionId] only if it is already in memory, without attempting to restore it. + * + * @param sessionId the [SessionId] of the session to retrieve. + * @return the cached client, or `null` if that session is not currently held in memory. */ fun getOrNull(sessionId: SessionId): MatrixClient? } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/SdkMetadata.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/SdkMetadata.kt index 16f34807fc..939734e314 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/SdkMetadata.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/SdkMetadata.kt @@ -8,6 +8,14 @@ package io.element.android.libraries.matrix.api +/** + * Exposes build information about the bundled `matrix-rust-sdk`. + * + * Mostly used to identify the exact SDK revision in bug reports and in the user agent. + */ interface SdkMetadata { + /** + * The full git SHA of the commit the bundled `matrix-rust-sdk` was built from. + */ val sdkGitSha: String } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/TemporaryMatrixClientFactory.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/TemporaryMatrixClientFactory.kt index 4be1b980a0..3bd22c64f3 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/TemporaryMatrixClientFactory.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/TemporaryMatrixClientFactory.kt @@ -11,5 +11,11 @@ package io.element.android.libraries.matrix.api * Factory to create a temporary [TemporaryMatrixClient] for a given home server URL. */ interface TemporaryMatrixClientFactory { + /** + * Builds an unauthenticated client pointing at [homeServerUrl], backed by a fresh set of temporary session directories. + * The caller owns the returned client and must close it to delete those directories. + * + * @param homeServerUrl the URL of the homeserver the client will talk to. + */ suspend fun create(homeServerUrl: String): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/GetDatabaseSizesUseCase.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/GetDatabaseSizesUseCase.kt index fde02fb2c6..1ca9adfaca 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/GetDatabaseSizesUseCase.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/GetDatabaseSizesUseCase.kt @@ -9,6 +9,14 @@ package io.element.android.libraries.matrix.api.analytics import io.element.android.libraries.matrix.api.core.SessionId +/** + * Reads the on-disk size of a session's SDK stores, so they can be reported as analytics. + */ fun interface GetDatabaseSizesUseCase { + /** + * Returns the size of each store of the given session. + * + * @param sessionId the session whose stores are measured. + */ operator fun invoke(sessionId: SessionId): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt index 04d1d13593..3ff1eaa337 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt @@ -16,23 +16,41 @@ import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId +/** + * Creates and restores sessions: password login, OAuth login, QR code login, and importing a session from another client. + * + * [setHomeserver] must be called before any login attempt, since it is what tells the SDK which server to authenticate against. + * Every successful login stores the session and notifies the listeners registered through [listenToNewMatrixClients]. + */ interface MatrixAuthenticationService { /** * Restore a session from a [sessionId]. * Do not restore anything it the access token is not valid anymore. * Generally this method should not be used directly, prefer using [MatrixClientProvider.getOrRestore] instead. + * + * @param sessionId the session to restore. */ suspend fun restoreSession(sessionId: SessionId): Result /** * Set the homeserver to use for authentication, and return its details. + * + * @param homeserver the homeserver URL or server name to authenticate against. */ suspend fun setHomeserver(homeserver: String): Result + /** + * Logs in with a password on the homeserver previously set through [setHomeserver]. + * + * @param username the user id or its local part. + * @param password the account password. + */ suspend fun login(username: String, password: String): Result /** * Import a session that was created using another client, for instance Element Web. + * + * @param externalSession the tokens and identifiers of the session to import. */ suspend fun importCreatedSession(externalSession: ExternalSession): Result @@ -42,6 +60,9 @@ interface MatrixAuthenticationService { /** * Get the OAuth url to display to the user. + * + * @param prompt whether the authentication server should show the sign-in or the account-creation page. + * @param loginHint the user identifier to pre-fill on that page, or `null` to leave it empty. */ suspend fun getOAuthUrl( prompt: OAuthPrompt, @@ -55,11 +76,17 @@ interface MatrixAuthenticationService { /** * Set the existing data about Element Classic session, if any. + * + * @param session the session found in a local Element Classic install, or `null` when there is none to migrate. */ fun setElementClassicSession(session: ElementClassicSession?) /** * Check if the provided secrets from Element Classic session contain a key backup. + * + * @param userId the user the secrets belong to. + * @param secrets the raw secrets exported from Element Classic. + * @param backupInfo the raw backup description exported alongside them. */ fun doSecretsContainBackupKey( userId: UserId, @@ -69,11 +96,23 @@ interface MatrixAuthenticationService { /** * Attempt to log in using the [callbackUrl] provided by the OAuth page. + * + * @param callbackUrl the redirect URL the authentication server sent the user back to, carrying the authorization code. */ suspend fun loginWithOAuth(callbackUrl: String): Result + /** + * Logs in by acting on a QR code shown by an already signed in device, reporting each step of the exchange as it happens. + * + * @param qrCodeData the decoded QR code, obtained from [io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDataFactory]. + * @param progress called on every step of the flow, so the UI can follow along. + */ suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit): Result - /** Listen to new Matrix clients being created on authentication. */ + /** + * Listen to new Matrix clients being created on authentication. + * + * @param lambda called with each client created by a successful login or session restore. + */ fun listenToNewMatrixClients(lambda: (MatrixClient) -> Unit) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthRedirectUrlProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthRedirectUrlProvider.kt index 669d47501d..648884e930 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthRedirectUrlProvider.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OAuthRedirectUrlProvider.kt @@ -8,6 +8,10 @@ package io.element.android.libraries.matrix.api.auth +/** + * Provides the redirect URL registered for OAuth login, which the authentication server sends the user back to once they have signed in. + */ interface OAuthRedirectUrlProvider { + /** Returns the redirect URL, built from the app's custom URL scheme so that the system hands the callback back to the app. */ fun provide(): String } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginData.kt index 413f17af63..f800ad2ba1 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginData.kt @@ -8,6 +8,12 @@ package io.element.android.libraries.matrix.api.auth.qrlogin +/** + * The decoded content of a login QR code, as produced by [MatrixQrCodeLoginDataFactory]. + * + * The underlying data is opaque to the app: it is passed back to the SDK to perform the login. + */ interface MatrixQrCodeLoginData { + /** The homeserver advertised by the QR code, or `null` when it does not carry one. */ fun serverName(): String? } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginDataFactory.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginDataFactory.kt index b0e3089501..21d77b74f2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginDataFactory.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/qrlogin/MatrixQrCodeLoginDataFactory.kt @@ -8,6 +8,14 @@ package io.element.android.libraries.matrix.api.auth.qrlogin +/** + * Decodes the raw bytes of a scanned QR code into login data the SDK can use. + */ interface MatrixQrCodeLoginDataFactory { + /** + * Parses the scanned bytes, failing with a [QrCodeDecodeException] when they are not a valid Matrix login QR code. + * + * @param data the raw content read from the QR code. + */ fun parseQrCodeData(data: ByteArray): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ProgressCallback.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ProgressCallback.kt index a7abc0f797..5c8a75e384 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ProgressCallback.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ProgressCallback.kt @@ -8,6 +8,15 @@ package io.element.android.libraries.matrix.api.core +/** + * Reports the progress of a long running transfer, such as a media upload or download. + */ interface ProgressCallback { + /** + * Called as the transfer advances, possibly many times and from a background thread. + * + * @param current the number of bytes transferred so far. + * @param total the total number of bytes to transfer. + */ fun onProgress(current: Long, total: Long) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SendHandle.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SendHandle.kt index f3b8904fff..02c8a85d58 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SendHandle.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SendHandle.kt @@ -8,6 +8,10 @@ package io.element.android.libraries.matrix.api.core +/** + * A handle on an event sitting in a room's send queue, obtained from a timeline item that failed to send. + */ fun interface SendHandle { + /** Puts the event back in the send queue for another attempt. */ suspend fun retry(): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt index f82cdc1969..e036a198df 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt @@ -14,13 +14,32 @@ import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +/** + * Gives access to the end-to-end encryption state of the session: key backup, recovery, and the trust of other users' identities. + */ interface EncryptionService { + /** + * The state of the room key backup on the server. + * Reports [BackupState.WAITING_FOR_SYNC] for as long as the session is not actively syncing, since the real state is unknown until then. + */ val backupStateStateFlow: StateFlow + + /** + * The state of recovery, i.e. whether the secrets needed to restore the keys on a new device are available. + * As with [backupStateStateFlow], it reports a waiting-for-sync state until the session is syncing. + */ val recoveryStateStateFlow: StateFlow + + /** Progress of the [enableRecovery] call currently in flight, to be observed while that call is running. */ val enableRecoveryProgressStateFlow: StateFlow + + /** Whether this is the only device of the account, which matters because logging out of it can make the room keys unrecoverable. */ val isLastDevice: StateFlow + + /** Whether the account has another device this one could be verified against; it stays loading until the first answer arrives. */ val hasDevicesToVerifyAgainst: StateFlow> + /** Enables the room key backup on the server, so that the keys survive losing every device. */ suspend fun enableBackups(): Result /** @@ -39,12 +58,16 @@ interface EncryptionService { */ suspend fun resetRecoveryKey(): Result + /** Disables recovery, which deletes the secrets from server-side storage; the key backup itself is left in place. */ suspend fun disableRecovery(): Result + /** Whether a room key backup currently exists on the server, whether or not this device is the one using it. */ suspend fun doesBackupExistOnServer(): Result /** - * Note: accept both recoveryKey and passphrase. + * Restores the secrets from server-side storage, which unlocks the room key backup for this device. + * + * @param recoveryKey the recovery key, or the passphrase it was derived from; both are accepted. */ suspend fun recover(recoveryKey: String): Result @@ -72,6 +95,8 @@ interface EncryptionService { /** * Remember this identity, ensuring it does not result in a pin violation. + * + * @param userId the user whose current identity should be pinned. */ suspend fun pinUserIdentity(userId: UserId): Result @@ -81,6 +106,8 @@ interface EncryptionService { * Useful when a user that was verified is not anymore, but it is not * possible to re-verify immediately. This allows to restore communication by reverting the * user trust from verified to TOFU verified. + * + * @param userId the user whose verification should be withdrawn. */ suspend fun withdrawVerification(userId: UserId): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/CheckCodeSender.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/CheckCodeSender.kt index ecb440de83..a3a518f9ce 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/CheckCodeSender.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/CheckCodeSender.kt @@ -7,6 +7,9 @@ package io.element.android.libraries.matrix.api.linknewdevice +/** + * Lets the user confirm the two-digit check code displayed by the device being linked, which proves the channel is not being intercepted. + */ interface CheckCodeSender { /** * Validates the given [code]. Returns true if the code is valid, false otherwise. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/ContinuationMessageSender.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/ContinuationMessageSender.kt index 0bbbaf3187..0a2ce273d5 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/ContinuationMessageSender.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/ContinuationMessageSender.kt @@ -7,7 +7,15 @@ package io.element.android.libraries.matrix.api.linknewdevice +/** + * Lets the user confirm or refuse a device linking request once they have approved it in the browser. + * + * Handed out with the step that is waiting for authorisation; exactly one of the two methods is meant to be called. + */ interface ContinuationMessageSender { + /** Tells the other device that the user refused the linking, which ends the flow. */ suspend fun cancel(): Result + + /** Tells the other device that the user approved the linking, so the flow can carry on. */ suspend fun confirm(): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/LinkDesktopHandler.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/LinkDesktopHandler.kt index 343624aa34..26c2ed04c4 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/LinkDesktopHandler.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/LinkDesktopHandler.kt @@ -10,8 +10,22 @@ package io.element.android.libraries.matrix.api.linknewdevice import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeDecodeException import kotlinx.coroutines.flow.StateFlow +/** + * Drives the linking of a new device that cannot scan a QR code, such as a desktop: this device scans the code the other one displays. + * + * Progress is reported entirely through [linkDesktopStep]; there is one handler per attempt, obtained from + * [io.element.android.libraries.matrix.api.MatrixClient.createLinkDesktopHandler]. + */ interface LinkDesktopHandler { + /** Each step of the flow, from establishing the secure channel to syncing the secrets to the new device. */ val linkDesktopStep: StateFlow + + /** + * Runs the whole flow from the QR code just scanned, suspending until it finishes or fails. + * A code that cannot be decoded is reported as an invalid-QR-code step rather than as a thrown error. + * + * @param data the raw content read from the QR code shown by the other device. + */ suspend fun handleScannedQrCode(data: ByteArray) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/LinkMobileHandler.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/LinkMobileHandler.kt index f187b8275a..ae51eb1f0a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/LinkMobileHandler.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/linknewdevice/LinkMobileHandler.kt @@ -9,8 +9,17 @@ package io.element.android.libraries.matrix.api.linknewdevice import kotlinx.coroutines.flow.Flow +/** + * Drives the linking of a new device that is able to scan a QR code, by showing one on this already signed in device. + * + * Progress is reported entirely through [linkMobileStep]; there is one handler per attempt, obtained from + * [io.element.android.libraries.matrix.api.MatrixClient.createLinkMobileHandler]. + */ interface LinkMobileHandler { + /** Each step of the flow, from generating the QR code to syncing the secrets to the new device. */ val linkMobileStep: Flow + + /** Runs the whole flow, suspending until it finishes or fails; the outcome is reported through [linkMobileStep]. */ suspend fun start() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt index a36849b812..28ae0762ff 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt @@ -8,6 +8,9 @@ package io.element.android.libraries.matrix.api.media +/** + * Fetches media content from the homeserver, decrypting it on the way when the room is encrypted. + */ interface MatrixMediaLoader { /** * @param source to fetch the content for. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaFile.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaFile.kt index b5cefe897b..ae6035da88 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaFile.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaFile.kt @@ -16,6 +16,7 @@ import java.io.File * When closed the file will be removed from the disk unless [persist] has been used. */ interface MediaFile : Closeable { + /** The absolute path of the file on disk; see [toFile] to get a [File] instead. */ fun path(): String /** diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewService.kt index 39718ddfd2..660bb0b80e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaPreviewService.kt @@ -10,6 +10,11 @@ package io.element.android.libraries.matrix.api.media import kotlinx.coroutines.flow.StateFlow +/** + * Reads and writes the account settings that decide whether media previews and invite avatars are shown. + * + * These are account data settings shared across clients, so the local value is only updated once the server has accepted the change. + */ interface MediaPreviewService { /** * Will fetch the media preview config from the server. @@ -24,11 +29,15 @@ interface MediaPreviewService { /** * Set the media preview display policy. This will update the value on the server and update the local value when successful. + * + * @param mediaPreviewValue whether media previews are shown always, never, or only in private rooms. */ suspend fun setMediaPreviewValue(mediaPreviewValue: MediaPreviewValue): Result /** * Set the invite avatars display policy. This will update the value on the server and update the local value when successful. + * + * @param hide true to hide the avatars of users and rooms the user has been invited by. */ suspend fun setHideInviteAvatars(hide: Boolean): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/mxc/MxcTools.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/mxc/MxcTools.kt index 09ceaa4712..c28bfa9933 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/mxc/MxcTools.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/mxc/MxcTools.kt @@ -8,6 +8,9 @@ package io.element.android.libraries.matrix.api.mxc +/** + * Helpers to work with Matrix Content (`mxc://`) URIs. + */ interface MxcTools { /** * Sanitizes an mxcUri to be used as a relative file path. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt index 369587cd10..3a3fbeaa19 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt @@ -24,6 +24,9 @@ typealias GetNotificationDataResult = Result>): GetNotificationDataResult } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt index 7ae1ca4886..7226de9a9d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notificationsettings/NotificationSettingsService.kt @@ -11,28 +11,114 @@ package io.element.android.libraries.matrix.api.notificationsettings import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationSettings -import io.element.android.libraries.matrix.api.room.RoomNotificationSettingsState import kotlinx.coroutines.flow.SharedFlow +/** + * Reads and writes the push rules of the account, which decide what generates a notification. + * + * Rules are account data shared across clients, so a change made here is visible on the user's other devices. + * Several methods take the encryption and one-to-one nature of a room, because Matrix keeps a separate default rule for each combination. + */ interface NotificationSettingsService { /** - * State of the current room notification settings flow ([RoomNotificationSettingsState.Unknown] if not started). + * Emits every time the push rules change, whether from this client or another one, without saying what changed. + * Callers are expected to re-read the values they display; only the most recent signal is buffered. */ val notificationSettingsChangeFlow: SharedFlow + + /** + * Returns the notification settings that apply to one room, which may come from a room specific rule or from the matching default. + * + * @param roomId the room to read the settings of. + * @param isEncrypted whether that room is encrypted, which selects the relevant default rule. + * @param isOneToOne whether that room is a direct message, which selects the relevant default rule. + */ suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result + + /** + * Returns the default notification mode used by rooms that have no rule of their own. + * + * @param isEncrypted whether to read the default for encrypted rooms. + * @param isOneToOne whether to read the default for direct messages. + */ suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result + + /** + * Changes the default notification mode for a whole category of rooms. + * + * @param isEncrypted whether to change the default for encrypted rooms. + * @param mode the new default mode. + * @param isDM whether to change the default for direct messages. + */ suspend fun setDefaultRoomNotificationMode(isEncrypted: Boolean, mode: RoomNotificationMode, isDM: Boolean): Result + + /** + * Sets a room specific notification mode, which from then on overrides the default for that room. + * + * @param roomId the room to change. + * @param mode the new mode for that room. + */ suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result + + /** + * Removes the room specific rule so the room follows the matching default again. + * + * @param roomId the room to reset. + */ suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result + + /** + * Mutes a room, which is a room specific rule like any other and is undone by [unmuteRoom]. + * + * @param roomId the room to mute. + */ suspend fun muteRoom(roomId: RoomId): Result + + /** + * Unmutes a room, restoring either its previous room specific mode or the matching default. + * + * @param roomId the room to unmute. + * @param isEncrypted whether that room is encrypted, which selects the relevant default rule. + * @param isOneToOne whether that room is a direct message, which selects the relevant default rule. + */ suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result + + /** Whether being mentioned through `@room` generates a notification. */ suspend fun isRoomMentionEnabled(): Result + + /** + * Sets whether being mentioned through `@room` generates a notification. + * + * @param enabled true to be notified of `@room` mentions. + */ suspend fun setRoomMentionEnabled(enabled: Boolean): Result + + /** Whether incoming calls generate a notification. */ suspend fun isCallEnabled(): Result + + /** + * Sets whether incoming calls generate a notification. + * + * @param enabled true to be notified of incoming calls. + */ suspend fun setCallEnabled(enabled: Boolean): Result + + /** Whether room invitations generate a notification. */ suspend fun isInviteForMeEnabled(): Result + + /** + * Sets whether room invitations generate a notification. + * + * @param enabled true to be notified of invitations. + */ suspend fun setInviteForMeEnabled(enabled: Boolean): Result + + /** The rooms that have an enabled rule of their own rather than following the defaults. */ suspend fun getRoomsWithUserDefinedRules(): Result> + + /** Whether the homeserver is able to push encrypted events to the device, which decides how much a notification can show before decryption. */ suspend fun canHomeServerPushEncryptedEventsToDevice(): Result + + /** The push rules as raw JSON, for debugging and bug reports; `null` when the server returned none. */ suspend fun getRawPushRules(): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverter.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverter.kt index cc3e2b5cf6..d6a2e71b8f 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverter.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/MatrixToConverter.kt @@ -20,6 +20,9 @@ interface MatrixToConverter { * - https://riot.im/develop/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org * - https://app.element.io/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org * - https://www.example.org/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org + * + * @param uri the uri to convert. + * @return the equivalent matrix.to uri, or `null` when the input is not a permalink this converter recognises. */ fun convert(uri: Uri): Uri? } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt index 878b0e7a69..9140c312c7 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt @@ -11,8 +11,22 @@ package io.element.android.libraries.matrix.api.permalink import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.UserId +/** + * Builds `matrix.to` permalinks, which are the shareable links to a user or a room; see [PermalinkParser] for the other direction. + */ interface PermalinkBuilder { + /** + * Builds a permalink to a user, failing with [PermalinkBuilderError.InvalidData] when the id is malformed. + * + * @param userId the user to link to. + */ fun permalinkForUser(userId: UserId): Result + + /** + * Builds a permalink to a room from its alias, failing with [PermalinkBuilderError.InvalidData] when the alias is malformed. + * + * @param roomAlias the room alias to link to. + */ fun permalinkForRoomAlias(roomAlias: RoomAlias): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt index 5365b8776a..467b147d98 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkParser.kt @@ -18,7 +18,10 @@ package io.element.android.libraries.matrix.api.permalink interface PermalinkParser { /** * Turns a uri string to a [PermalinkData]. + * A uri that is not a Matrix permalink is reported as a fallback link rather than as an error, so this never throws. * https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md + * + * @param uriString the uri to parse. */ fun parse(uriString: String): PermalinkData } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt index 6764598235..1eafe8ab49 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt @@ -8,7 +8,21 @@ package io.element.android.libraries.matrix.api.pusher +/** + * Registers and removes the pusher of this session, i.e. the HTTP gateway the homeserver sends push notifications to. + */ interface PushersService { + /** + * Registers or updates the pusher; calling it again with the same push key replaces the existing registration. + * + * @param setHttpPusherData the gateway URL, the push key and the other details the homeserver needs to reach the device. + */ suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result + + /** + * Removes the pusher, so the homeserver stops sending push notifications to this device. + * + * @param unsetHttpPusherData the push key and app id identifying the registration to remove. + */ suspend fun unsetHttpPusher(unsetHttpPusherData: UnsetHttpPusherData): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt index d49343f998..ab65b00f08 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/BaseRoom.kt @@ -66,21 +66,30 @@ interface BaseRoom : Closeable { */ fun isDm() = roomInfoFlow.value.isDm + /** + * The room this one replaced when it was upgraded, or `null` if this room has no predecessor. + * Errors are swallowed and reported as `null`. + */ fun predecessorRoom(): PredecessorRoom? /** * Try to load the room members and update the membersFlow. + * The first call may serve members from the local cache before the server response arrives; later calls always go to the server. */ suspend fun updateMembers() /** * Get the members of the room. Note: generally this should not be used, please use * [membersStateFlow] and [updateMembers] instead. + * + * @param limit the maximum number of members to return. */ suspend fun getMembers(limit: Int = 5): Result> /** * Will return an updated member or an error. + * + * @param userId the member to fetch. */ suspend fun getUpdatedMember(userId: UserId): Result @@ -103,6 +112,8 @@ interface BaseRoom : Closeable { /** * Gets the role of the user with the provided [userId] in the room. + * + * @param userId the member whose role is requested. */ suspend fun userRole(userId: UserId): Result @@ -113,11 +124,15 @@ interface BaseRoom : Closeable { /** * Gets the display name of the user with the provided [userId] in the room. + * + * @param userId the member whose room specific display name is requested. */ suspend fun userDisplayName(userId: UserId): Result /** * Gets the avatar of the user with the provided [userId] in the room. + * + * @param userId the member whose room specific avatar is requested. */ suspend fun userAvatarUrl(userId: UserId): Result @@ -138,6 +153,8 @@ interface BaseRoom : Closeable { /** * Sets the room as favorite or not, based on the [isFavorite] parameter. + * + * @param isFavorite true to mark the room as favorite, false to remove the flag. */ suspend fun setIsFavorite(isFavorite: Boolean): Result @@ -176,22 +193,29 @@ interface BaseRoom : Closeable { suspend fun getRoomVisibility(): Result /** - * Returns the visibility for this room in the room directory, fetching it from the homeserver if needed. + * Returns whether this room is encrypted, based on the latest encryption state known to the SDK rather than on the cached [RoomInfo]. */ suspend fun getUpdatedIsEncrypted(): Result /** * Store the given `ComposerDraft` in the state store of this room. + * + * @param composerDraft the unsent message content to remember. + * @param threadRoot the thread the draft belongs to, or `null` for the main timeline. */ suspend fun saveComposerDraft(composerDraft: ComposerDraft, threadRoot: ThreadId?): Result /** * Retrieve the `ComposerDraft` stored in the state store for this room. + * + * @param threadRoot the thread whose draft is requested, or `null` for the main timeline. */ suspend fun loadComposerDraft(threadRoot: ThreadId?): Result /** * Clear the `ComposerDraft` stored in the state store for this room. + * + * @param threadRoot the thread whose draft should be cleared, or `null` for the main timeline. */ suspend fun clearComposerDraft(threadRoot: ThreadId?): Result @@ -202,10 +226,27 @@ interface BaseRoom : Closeable { */ suspend fun reportRoom(reason: String?): Result + /** + * Declines the incoming call advertised by the given notification, letting the other devices of the room know. + * + * @param notificationEventId the id of the call notification event being declined. + */ suspend fun declineCall(notificationEventId: EventId): Result + /** + * Emits the id of each user who declines the call advertised by the given notification. + * Used to stop ringing once the callee has declined from another device. + * + * @param notificationEventId the id of the call notification event to watch. + */ suspend fun subscribeToCallDecline(notificationEventId: EventId): Flow + /** + * Returns the id of the thread the given event belongs to, fetching the event from the server if it is not cached locally. + * + * @param eventId the event whose thread is requested. + * @return the thread root id, or `null` if the event is not part of a thread. + */ suspend fun threadRootIdForEvent(eventId: EventId): Result /** @@ -213,5 +254,6 @@ interface BaseRoom : Closeable { */ fun destroy() + /** Same as [destroy], so that a room can be used with `use { }`. */ override fun close() = destroy() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt index 6d8325427b..98498131f9 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/JoinedRoom.kt @@ -28,11 +28,28 @@ import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +/** + * A room the current user has joined, adding to [BaseRoom] everything that requires membership: sending events, moderation and room settings. + * + * An instance owns SDK resources, in particular [liveTimeline], so it must be closed once no longer needed. + */ interface JoinedRoom : BaseRoom { + /** + * A counter incremented every time a synced event is received on [liveTimeline], so callers can refresh data derived from the room. + * It carries no meaning beyond "something changed", and is only updated while it has subscribers. + */ val syncUpdateFlow: StateFlow + /** The members currently typing in the room, never including the current user; starts as an empty list. */ val roomTypingMembersFlow: Flow> + + /** Emits when the verification state of other members' identities changes, for instance after a member resets their identity. */ val identityStateChangesFlow: Flow> + + /** + * The notification settings that apply to this room. + * Starts as [RoomNotificationSettingsState.Unknown]: [updateRoomNotificationSettings] must be called to populate it. + */ val roomNotificationSettingsStateFlow: StateFlow /** @@ -45,6 +62,7 @@ interface JoinedRoom : BaseRoom { */ val liveTimeline: Timeline + /** Gives access to the paginated list of threads started in this room. */ val threadsListService: ThreadsListService /** @@ -55,6 +73,14 @@ interface JoinedRoom : BaseRoom { createTimelineParams: CreateTimelineParams, ): Result + /** + * Replaces the content of an event previously sent by the current user, by sending an edit of it. + * + * @param eventId the event to edit. + * @param body the new content, as plain text. + * @param htmlBody the new content as HTML, or `null` when the message has no formatted version. + * @param intentionalMentions the users and rooms this new content deliberately mentions. + */ suspend fun editMessage(eventId: EventId, body: String, htmlBody: String?, intentionalMentions: List): Result /** @@ -63,12 +89,28 @@ interface JoinedRoom : BaseRoom { */ suspend fun typingNotice(isTyping: Boolean): Result + /** + * Invites a user to this room. + * + * @param id the user to invite. + */ suspend fun inviteUserById(id: UserId): Result + /** + * Uploads [data] as the new avatar of the room, not of the current user. + * + * @param mimeType the MIME type of the image, for instance `image/jpeg`. + * @param data the raw bytes of the image. + */ suspend fun updateAvatar(mimeType: String, data: ByteArray): Result + /** Removes the avatar of the room. */ suspend fun removeAvatar(): Result + /** + * Refreshes [roomNotificationSettingsStateFlow], which moves to a pending state and then to a ready or error state. + * The failure is reported through that flow as well as through the returned [Result]. + */ suspend fun updateRoomNotificationSettings(): Result /** @@ -83,11 +125,15 @@ interface JoinedRoom : BaseRoom { /** * Update the room's visibility in the room directory. + * + * @param roomVisibility whether the room should be listed publicly in the directory. */ suspend fun updateRoomVisibility(roomVisibility: RoomVisibility): Result /** - * Update room history visibility for this room. + * Update room history visibility for this room, i.e. how much of the past a new member is allowed to read. + * + * @param historyVisibility the new history visibility to apply. */ suspend fun updateHistoryVisibility(historyVisibility: RoomHistoryVisibility): Result @@ -98,6 +144,8 @@ interface JoinedRoom : BaseRoom { * - `true` if the room alias didn't exist and it's now published. * - `false` if the room alias was already present so it couldn't be * published. + * + * @param roomAlias the alias to publish. */ suspend fun publishRoomAliasInRoomDirectory(roomAlias: RoomAlias): Result @@ -108,6 +156,8 @@ interface JoinedRoom : BaseRoom { * - `true` if the room alias was present and it's now removed from the * room directory. * - `false` if the room alias didn't exist so it couldn't be removed. + * + * @param roomAlias the alias to remove. */ suspend fun removeRoomAliasFromRoomDirectory(roomAlias: RoomAlias): Result @@ -117,26 +167,74 @@ interface JoinedRoom : BaseRoom { suspend fun enableEncryption(): Result /** - * Update the join rule for this room. + * Update the join rule for this room, i.e. who is allowed to join it. + * + * @param joinRule the new join rule to apply. */ suspend fun updateJoinRule(joinRule: JoinRule): Result + /** + * Changes the power level of individual members, which is how roles such as moderator or administrator are granted. + * + * @param changes the new power level to apply to each user. + */ suspend fun updateUsersRoles(changes: List): Result + /** + * Changes the power level required for each action in the room, such as inviting, kicking or changing the room name. + * + * @param roomPowerLevelsValues the new threshold for every action. + */ suspend fun updatePowerLevels(roomPowerLevelsValues: RoomPowerLevelsValues): Result + /** Restores the power levels required for each action to the defaults the room was created with. */ suspend fun resetPowerLevels(): Result + /** + * Changes the name of the room. + * + * @param name the new room name. + */ suspend fun setName(name: String): Result + /** + * Changes the topic of the room. + * + * @param topic the new room topic. + */ suspend fun setTopic(topic: String): Result + /** + * Reports an event to the homeserver moderators, optionally ignoring its sender at the same time. + * + * @param eventId the event being reported. + * @param reason the explanation sent to the moderators. + * @param blockUserId the sender to also add to the ignore list, or `null` to only report the event. + */ suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result + /** + * Removes a member from the room; they are able to join again unless they are also banned. + * + * @param userId the member to remove. + * @param reason the reason recorded in the membership event, or `null` for none. + */ suspend fun kickUser(userId: UserId, reason: String? = null): Result + /** + * Bans a user from the room, removing them if they are a member and preventing them from joining again. + * + * @param userId the user to ban. + * @param reason the reason recorded in the membership event, or `null` for none. + */ suspend fun banUser(userId: UserId, reason: String? = null): Result + /** + * Lifts the ban on a user, allowing them to join the room again; it does not invite them back. + * + * @param userId the user to unban. + * @param reason the reason recorded in the membership event, or `null` for none. + */ suspend fun unbanUser(userId: UserId, reason: String? = null): Result /** @@ -161,6 +259,12 @@ interface JoinedRoom : BaseRoom { */ fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result + /** + * Enables or disables the send queue of this room only; see [io.element.android.libraries.matrix.api.MatrixClient.setAllSendQueuesEnabled] + * for the session-wide equivalent and for why a queue disables itself. + * + * @param enabled whether the send queue of this room should be enabled. + */ suspend fun setSendQueueEnabled(enabled: Boolean) /** diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/NotJoinedRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/NotJoinedRoom.kt index fd5e71ff9d..a47179cae7 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/NotJoinedRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/NotJoinedRoom.kt @@ -12,7 +12,13 @@ import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo /** A reference to a room either invited, knocked or banned. */ interface NotJoinedRoom : AutoCloseable { + /** What is publicly known about the room: name, topic, avatar, join rule and member count. */ val previewInfo: RoomPreviewInfo + + /** + * The local room, available when the user has a membership in it, for instance after being invited. + * It is `null` for a room only discovered through a preview, and closing this instance also closes it. + */ val localRoom: BaseRoom? /** diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/RoomAliasHelper.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/RoomAliasHelper.kt index c23252961f..aecd2ab7f4 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/RoomAliasHelper.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/RoomAliasHelper.kt @@ -10,7 +10,21 @@ package io.element.android.libraries.matrix.api.room.alias import io.element.android.libraries.matrix.api.core.RoomAlias +/** + * Helpers to build and validate room aliases, delegating to the SDK so that the app and the server agree on what is acceptable. + */ interface RoomAliasHelper { + /** + * Turns a room display name into a usable alias local part, dropping the characters that are not allowed in an alias. + * + * @param name the room display name to derive the alias from. + */ fun roomAliasNameFromRoomDisplayName(name: String): String + + /** + * Whether the given alias is well-formed; this is a format check only and says nothing about the alias being taken. + * + * @param roomAlias the alias to validate. + */ fun isRoomAliasValid(roomAlias: RoomAlias): Boolean } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRoom.kt index 4220b68f0d..a6b1f3d36b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRoom.kt @@ -11,7 +11,18 @@ package io.element.android.libraries.matrix.api.room.join import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +/** + * Joins a room and reports the join to analytics, so that callers do not have to do both. + */ interface JoinRoom { + /** + * Joins the room, picking the right SDK call depending on whether an id or an alias was given, and captures an analytics event on success. + * A `Forbidden` answer from the server is translated into [Failures.UnauthorizedJoin] so callers can tell it apart from other errors. + * + * @param roomIdOrAlias the id or the alias of the room to join. + * @param serverNames servers to ask about the room, ignored when joining by alias. + * @param trigger what made the user join, recorded in the analytics event. + */ suspend operator fun invoke( roomIdOrAlias: RoomIdOrAlias, serverNames: List, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/knock/KnockRequest.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/knock/KnockRequest.kt index 317157793e..386be0d5c9 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/knock/KnockRequest.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/knock/KnockRequest.kt @@ -11,20 +11,50 @@ package io.element.android.libraries.matrix.api.room.knock import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId +/** + * A pending request from a user to join a room whose join rule is knock, along with the actions a moderator can take on it. + * + * Instances are snapshots handed out by [io.element.android.libraries.matrix.api.room.JoinedRoom.knockRequestsFlow]. + */ interface KnockRequest { + /** The id of the membership event carrying the request. */ val eventId: EventId + + /** The user asking to join. */ val userId: UserId + + /** The display name of the user asking to join, or `null` if they have none. */ val displayName: String? + + /** The avatar of the user asking to join, or `null` if they have none. */ val avatarUrl: String? + + /** The message the user attached to their request, or `null` if they sent none. */ val reason: String? + + /** When the request was made, in milliseconds since the epoch, or `null` if the server did not provide it. */ val timestamp: Long? + + /** Whether a moderator has already marked this request as seen, so that it is no longer highlighted as new. */ val isSeen: Boolean + /** Accepts the request, which makes the user a member of the room. */ suspend fun accept(): Result + /** + * Declines the request; the user is free to knock again. + * + * @param reason the explanation recorded in the membership event, or `null` for none. + */ suspend fun decline(reason: String?): Result + /** + * Declines the request and bans the user, so that they cannot knock again. + * + * @param reason the explanation recorded in the membership event, or `null` for none. + */ suspend fun declineAndBan(reason: String?): Result + /** Marks the request as seen, without accepting or declining it. */ suspend fun markAsSeen(): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPermissions.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPermissions.kt index a12f7d9606..19c01b9975 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPermissions.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/powerlevels/RoomPermissions.kt @@ -59,12 +59,16 @@ interface RoomPermissions : AutoCloseable { /** * Returns true if the current user is able to send a specific message type * in the room. + * + * @param message the kind of message the caller wants to send. */ fun canOwnUserSendMessage(message: MessageEventType): Boolean /** * Returns true if the current user is able to send a specific state event * type in the room. + * + * @param stateEvent the kind of state event the caller wants to send. */ fun canOwnUserSendState(stateEvent: StateEventType): Boolean @@ -77,48 +81,66 @@ interface RoomPermissions : AutoCloseable { /** * Returns true if the user with the given userId is able to ban in the * room. + * + * @param userId the member whose ability is checked. */ fun canUserBan(userId: UserId): Boolean /** * Returns true if the user with the given userId is able to invite in the * room. + * + * @param userId the member whose ability is checked. */ fun canUserInvite(userId: UserId): Boolean /** * Returns true if the user with the given userId is able to kick in the * room. + * + * @param userId the member whose ability is checked. */ fun canUserKick(userId: UserId): Boolean /** * Returns true if the user with the given userId is able to pin or unpin * events in the room. + * + * @param userId the member whose ability is checked. */ fun canUserPinUnpin(userId: UserId): Boolean /** * Returns true if the user with the given userId is able to redact * messages of other users in the room. + * + * @param userId the member whose ability is checked. */ fun canUserRedactOther(userId: UserId): Boolean /** * Returns true if the user with the given userId is able to redact * their own messages in the room. + * + * @param userId the member whose ability is checked. */ fun canUserRedactOwn(userId: UserId): Boolean /** * Returns true if the user with the given userId is able to send a * specific message type in the room. + * + * @param userId the member whose ability is checked. + * @param message the kind of message to check. */ fun canUserSendMessage(userId: UserId, message: MessageEventType): Boolean /** * Returns true if the user with the given userId is able to send a * specific state event type in the room. + * + * @param userId the member whose ability is checked. + * @param stateEvent the kind of state event to check. */ fun canUserSendState(userId: UserId, stateEvent: StateEventType): Boolean @@ -127,6 +149,8 @@ interface RoomPermissions : AutoCloseable { * notification in the room. * * The call may fail if there is an error in getting the power levels. + * + * @param userId the member whose ability is checked. */ fun canUserTriggerRoomNotification(userId: UserId): Boolean } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadsListService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadsListService.kt index 7f819c540c..f544211741 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadsListService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/threads/ThreadsListService.kt @@ -9,10 +9,27 @@ package io.element.android.libraries.matrix.api.room.threads import kotlinx.coroutines.flow.Flow +/** + * Exposes the paginated list of threads of a single room, kept up to date by the SDK. + * + * The list grows through explicit [paginate] calls; [destroy] must be called to release the underlying SDK resources. + */ interface ThreadsListService { + /** + * The threads loaded so far, starting empty and re-emitted in full whenever the SDK sends an update. + * The underlying subscription is started on the first call and shared by later ones. + */ fun subscribeToItemUpdates(): Flow> + + /** The pagination state of the list, emitting the current value immediately so callers know whether more threads can be loaded. */ fun subscribeToPaginationUpdates(): Flow + + /** Loads the next page of threads, which will be reflected in [subscribeToItemUpdates]. */ suspend fun paginate(): Result + + /** Discards the loaded threads and starts the list again from the most recent one. */ suspend fun reset(): Result + + /** Releases the SDK resources and stops the item subscription; the service is unusable afterwards. */ fun destroy() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt index c48bf54e9c..49de1bee89 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryList.kt @@ -10,12 +10,21 @@ package io.element.android.libraries.matrix.api.roomdirectory import kotlinx.coroutines.flow.Flow +/** + * A single, stateful search in the public room directory: one query at a time, with paginated results. + * + * Obtain an instance from [RoomDirectoryService.createRoomDirectoryList]. + */ interface RoomDirectoryList { /** * Starts a filtered search for the server. * If the filter is not provided it will search for all the rooms. You can specify a batch_size to control the number of rooms to fetch per request. * If the via_server is not provided it will search in the current homeserver by default. * This method will clear the current search results and start a new one + * + * @param filter the text to look for, or `null` to list every room. + * @param batchSize the number of rooms to fetch per request. + * @param viaServerName the server whose directory to search, or `null` to use the user's own homeserver. */ suspend fun filter(filter: String?, batchSize: Int, viaServerName: String?): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt index 8f71cfb18c..65f83299cb 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomdirectory/RoomDirectoryService.kt @@ -10,6 +10,14 @@ package io.element.android.libraries.matrix.api.roomdirectory import kotlinx.coroutines.CoroutineScope +/** + * Gives access to the public room directory, i.e. the rooms a server advertises to users who have not joined them. + */ interface RoomDirectoryService { + /** + * Creates an independent directory search, which is stateful and holds one query at a time. + * + * @param scope the lifetime of the search; the underlying SDK resources are released when it completes, so there is nothing to close by hand. + */ fun createRoomDirectoryList(scope: CoroutineScope): RoomDirectoryList } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt index 952cee6f22..eee98b82e9 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/roomlist/DynamicRoomList.kt @@ -14,6 +14,7 @@ package io.element.android.libraries.matrix.api.roomlist * It lets load rooms on demand and filter them. */ interface DynamicRoomList : RoomList { + /** The number of rooms added to the list by each [loadMore] call, as chosen when the list was created. */ val pageSize: Int /** diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/scanner/ContentScannerUrlProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/scanner/ContentScannerUrlProvider.kt index 76878080ae..2c11c750e4 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/scanner/ContentScannerUrlProvider.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/scanner/ContentScannerUrlProvider.kt @@ -19,7 +19,13 @@ fun interface ContentScannerUrlProvider { */ suspend fun getContentScannerUrl(sessionId: SessionId): Result + /** + * Creates a provider, which needs a fetcher because the scanner URL is itself discovered over HTTP. + */ fun interface Factory { + /** + * @param urlContentFetcher used to read the configuration advertised by the homeserver; a client that is not authenticated yet also works. + */ fun create(urlContentFetcher: UrlContentFetcher): ContentScannerUrlProvider } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/search/MessageSearchService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/search/MessageSearchService.kt index 3e20b163f2..d7d8c017e3 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/search/MessageSearchService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/search/MessageSearchService.kt @@ -10,6 +10,11 @@ package io.element.android.libraries.matrix.api.search import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.coroutines.CoroutineScope +/** + * Creates the message search cursors backed by the local search index. + * + * Only usable when the session was built with the index attached, see [io.element.android.libraries.matrix.api.MatrixClient.isMessageSearchAvailable]. + */ interface MessageSearchService { /** * Create a new, independent search cursor. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/server/UserServerResolver.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/server/UserServerResolver.kt index 3529b17d85..6c3c16b35e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/server/UserServerResolver.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/server/UserServerResolver.kt @@ -8,6 +8,10 @@ package io.element.android.libraries.matrix.api.server +/** + * Resolves the server name of the logged in user, which is the part of their user id after the colon. + */ interface UserServerResolver { + /** Returns the server name, for instance `matrix.org` for `@alice:matrix.org`. */ fun resolve(): String } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/LeaveSpaceHandle.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/LeaveSpaceHandle.kt index 174ec20dc9..59b9657b58 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/LeaveSpaceHandle.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/LeaveSpaceHandle.kt @@ -10,6 +10,11 @@ package io.element.android.libraries.matrix.api.spaces import io.element.android.libraries.matrix.api.core.RoomId +/** + * Handles leaving a space, letting the user also choose which of its rooms to leave in the same operation. + * + * Obtain an instance from [SpaceService.getLeaveSpaceHandle]; the caller owns it and must [close] it. + */ interface LeaveSpaceHandle { /** * The id of the space to leave. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomList.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomList.kt index 2ccee8d1ee..b29d721d07 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomList.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceRoomList.kt @@ -16,22 +16,36 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import java.util.Optional +/** + * The paginated list of the children of a single space, kept up to date by the SDK. + * + * Obtain an instance from [SpaceService.spaceRoomList]; the caller owns it and must call [destroy] when done. + */ interface SpaceRoomList { sealed interface PaginationStatus { data object Loading : PaginationStatus data class Idle(val hasMoreToLoad: Boolean) : PaginationStatus } + /** The space whose children this list holds. */ val spaceId: RoomId + /** What is known about the space itself, starting empty until the SDK provides it. */ val currentSpaceFlow: StateFlow> + /** The children loaded so far, re-emitted in full on every update; the latest value is replayed to new collectors. */ val spaceRoomsFlow: Flow> + + /** Whether a page is being loaded and whether more children remain; see [loadAllIncrementally] to drain the list. */ val paginationStatusFlow: StateFlow + /** Loads the next page of children, which will be reflected in [spaceRoomsFlow]. */ suspend fun paginate(): Result + + /** Discards the loaded children and starts the list again from the beginning. */ suspend fun reset(): Result + /** Releases the SDK resources and cancels the internal scope; the list is unusable afterwards. */ fun destroy() } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt index 299209e188..a7126a4766 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/spaces/SpaceService.kt @@ -11,13 +11,35 @@ package io.element.android.libraries.matrix.api.spaces import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.coroutines.flow.SharedFlow +/** + * Gives access to the spaces the user belongs to, their hierarchy, and the operations that change it. + */ interface SpaceService { + /** The spaces that have no joined parent, i.e. the roots of the hierarchy shown to the user; the latest value is replayed to new collectors. */ val topLevelSpacesFlow: SharedFlow> + + /** The filters the space list can be narrowed down with; as with [topLevelSpacesFlow], the latest value is replayed. */ val spaceFiltersFlow: SharedFlow> + + /** + * Returns the spaces the given room or space is a child of, restricted to the ones the user has joined. + * + * @param spaceId the room or space whose parents are requested. + */ suspend fun joinedParents(spaceId: RoomId): Result> + /** + * Returns what is known about a single space, or `null` if it is unknown or the lookup failed. + * + * @param spaceId the space to look up. + */ suspend fun getSpaceRoom(spaceId: RoomId): SpaceRoom? + /** + * Creates a paginated list of the children of a space; the caller owns it and must call `destroy` on it. + * + * @param id the space whose children are listed. + */ fun spaceRoomList(id: RoomId): SpaceRoomList /** @@ -25,6 +47,11 @@ interface SpaceService { */ suspend fun editableSpaces(): Result> + /** + * Creates a handle to leave a space, which lets the user also pick which of its rooms to leave; the caller must close it. + * + * @param spaceId the space the user wants to leave. + */ fun getLeaveSpaceHandle(spaceId: RoomId): LeaveSpaceHandle /** diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt index ce0019d8dc..92f60352d7 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt @@ -10,21 +10,31 @@ package io.element.android.libraries.matrix.api.sync import kotlinx.coroutines.flow.StateFlow +/** + * Drives the sync loop of a single session and exposes its current state. + * + * Both [startSync] and [stopSync] are idempotent and never throw: any error is wrapped in the returned [Result]. + */ interface SyncService { /** - * Tries to start the sync. If already syncing it has no effect. + * Tries to start the sync. If already syncing, or if the service has been destroyed, it has no effect. */ suspend fun startSync(): Result /** - * Tries to stop the sync. If service is not syncing it has no effect. + * Tries to stop the sync. If the service is not syncing, or has been destroyed, it has no effect. */ suspend fun stopSync(): Result /** * Flow of [SyncState]. Will be updated as soon as the current [SyncState] changes. + * Starts at [SyncState.Idle] and only emits on an actual change, for as long as the session lives. */ val syncState: StateFlow + /** + * Whether the client currently considers itself online, derived from [syncState]. + * This is `false` only while the state is [SyncState.Offline]; an idle or errored sync still counts as online. + */ val isOnline: StateFlow } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt index 8cb933205b..72483e68f1 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/Timeline.kt @@ -31,6 +31,12 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.parcelize.Parcelize import java.io.File +/** + * A window over the events of a room, restricted by its [Mode], and the entry point for sending events to that room. + * + * A timeline owns SDK resources and must be closed once no longer needed, except the live timeline, which is owned by its room. + * Sending returns successfully as soon as the event has been handed to the room's send queue, not when the server has accepted it. + */ interface Timeline : AutoCloseable { data class PaginationStatus( val isPaginating: Boolean, @@ -54,18 +60,66 @@ interface Timeline : AutoCloseable { data class Thread(val threadRootId: ThreadId) : Mode } + /** What this timeline is a view over: the live room, a single event and its context, the pinned events, the media, or a thread. */ val mode: Mode + + /** + * Emits every time a membership change event is received in this timeline, without saying which one. + * Collecting it keeps the timeline subscribed to the SDK, so it should not be collected for longer than needed. + */ val membershipChangeEventReceived: Flow + + /** + * Emits every time an event coming from sync is received in this timeline, without saying which one. + * As with [membershipChangeEventReceived], collecting it keeps the timeline subscribed to the SDK. + */ val onSyncedEventReceived: Flow + + /** + * Sends a read receipt for a single event, which is how the unread state of the room is cleared. + * + * @param eventId the event the receipt points at. + * @param receiptType whether the receipt is public, private or only marks the fully read position. + */ suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result + + /** + * Marks the whole timeline as read by sending a receipt on its latest event. + * + * @param receiptType whether the receipt is public, private or only marks the fully read position. + */ suspend fun markAsRead(receiptType: ReceiptType): Result + + /** + * Loads one more page of events and updates the matching pagination status flow. + * Fails with a `CannotPaginate` error when the corresponding status does not allow it, for instance because a pagination is already running. + * + * @param direction whether to load older or newer events. + * @return whether the end of the timeline has been reached in that direction. + */ suspend fun paginate(direction: PaginationDirection): Result + /** Whether older events are being loaded and whether more remain; pinned event timelines start with nothing more to load. */ val backwardPaginationStatus: StateFlow + + /** Whether newer events are being loaded and whether more remain; only a timeline focused on an event starts with more to load. */ val forwardPaginationStatus: StateFlow + /** + * The items to display, in timeline order, re-emitted on every change. + * Beyond the room events this also carries the virtual items the UI needs, such as date separators, loading indicators and the room beginning. + */ val timelineItems: Flow> + /** + * Sends a text message to the room this timeline belongs to. + * + * @param body the message content, as plain text. + * @param htmlBody the message content as HTML, or `null` to send it unformatted. + * @param intentionalMentions the users and rooms the message deliberately mentions, so that they are notified. + * @param msgType the Matrix message type to use, which distinguishes a normal message from an emote or a notice. + * @param asPlainText whether to send the message without any formatting, ignoring markdown in [body]. + */ suspend fun sendMessage( body: String, htmlBody: String?, @@ -74,6 +128,14 @@ interface Timeline : AutoCloseable { asPlainText: Boolean = false, ): Result + /** + * Replaces the content of a message, whether it has already been sent or is still local in the send queue. + * + * @param eventOrTransactionId the event to edit, identified by its event id once sent or by its transaction id while still local. + * @param body the new content, as plain text. + * @param htmlBody the new content as HTML, or `null` to send it unformatted. + * @param intentionalMentions the users and rooms the new content deliberately mentions. + */ suspend fun editMessage( eventOrTransactionId: EventOrTransactionId, body: String, @@ -81,12 +143,29 @@ interface Timeline : AutoCloseable { intentionalMentions: List, ): Result + /** + * Replaces the caption of an already sent media message, leaving the media itself untouched. + * + * @param eventOrTransactionId the media event to edit, identified by its event id or by its transaction id while still local. + * @param caption the new caption, or `null` to remove it. + * @param formattedCaption the new caption as HTML, or `null` to send it unformatted. + */ suspend fun editCaption( eventOrTransactionId: EventOrTransactionId, caption: String?, formattedCaption: String?, ): Result + /** + * Sends a message as a reply to another event. + * + * @param repliedToEventId the event being replied to. + * @param body the reply content, as plain text. + * @param htmlBody the reply content as HTML, or `null` to send it unformatted. + * @param intentionalMentions the users and rooms the reply deliberately mentions. + * @param fromNotification whether the reply was written from a notification rather than from the timeline. + * @param msgType the Matrix message type to use. + */ suspend fun replyMessage( repliedToEventId: EventId, body: String, @@ -96,6 +175,16 @@ interface Timeline : AutoCloseable { msgType: MsgType = MsgType.MSG_TYPE_TEXT, ): Result + /** + * Starts sending an image. The returned [MediaUploadHandler] tracks the upload and must be awaited by the caller to know the outcome. + * + * @param file the image to upload. + * @param thumbnailFile a pre-generated thumbnail, or `null` to send none. + * @param imageInfo the dimensions, size and MIME type of the image. + * @param caption the text shown along with the image, or `null` for none. + * @param formattedCaption the caption as HTML, or `null` to send it unformatted. + * @param inReplyToEventId the event this message replies to, or `null` if it is not a reply. + */ suspend fun sendImage( file: File, thumbnailFile: File?, @@ -105,6 +194,16 @@ interface Timeline : AutoCloseable { inReplyToEventId: EventId?, ): Result + /** + * Starts sending a video, with the same upload contract as [sendImage]. + * + * @param file the video to upload. + * @param thumbnailFile a pre-generated thumbnail, or `null` to send none. + * @param videoInfo the duration, dimensions, size and MIME type of the video. + * @param caption the text shown along with the video, or `null` for none. + * @param formattedCaption the caption as HTML, or `null` to send it unformatted. + * @param inReplyToEventId the event this message replies to, or `null` if it is not a reply. + */ suspend fun sendVideo( file: File, thumbnailFile: File?, @@ -114,6 +213,15 @@ interface Timeline : AutoCloseable { inReplyToEventId: EventId?, ): Result + /** + * Starts sending an audio file, with the same upload contract as [sendImage]; use [sendVoiceMessage] for a recorded voice message. + * + * @param file the audio file to upload. + * @param audioInfo the duration, size and MIME type of the audio. + * @param caption the text shown along with the audio, or `null` for none. + * @param formattedCaption the caption as HTML, or `null` to send it unformatted. + * @param inReplyToEventId the event this message replies to, or `null` if it is not a reply. + */ suspend fun sendAudio( file: File, audioInfo: AudioInfo, @@ -122,6 +230,15 @@ interface Timeline : AutoCloseable { inReplyToEventId: EventId?, ): Result + /** + * Starts sending an arbitrary file, with the same upload contract as [sendImage]. + * + * @param file the file to upload. + * @param fileInfo the size and MIME type of the file. + * @param caption the text shown along with the file, or `null` for none. + * @param formattedCaption the caption as HTML, or `null` to send it unformatted. + * @param inReplyToEventId the event this message replies to, or `null` if it is not a reply. + */ suspend fun sendFile( file: File, fileInfo: FileInfo, @@ -151,6 +268,14 @@ interface Timeline : AutoCloseable { inReplyToEventId: EventId?, ): Result + /** + * Starts sending a recorded voice message, with the same upload contract as [sendImage]. + * + * @param file the recorded audio to upload. + * @param audioInfo the duration, size and MIME type of the recording. + * @param waveform the amplitudes used to draw the waveform, normalised between 0 and 1. + * @param inReplyToEventId the event this message replies to, or `null` if it is not a reply. + */ suspend fun sendVoiceMessage( file: File, audioInfo: AudioInfo, @@ -158,6 +283,14 @@ interface Timeline : AutoCloseable { inReplyToEventId: EventId?, ): Result + /** + * Starts sending several media items as a single gallery message, with the same upload contract as [sendImage]. + * + * @param items the media making up the gallery, in the order they should appear. + * @param caption the text shown along with the gallery, or `null` for none. + * @param formattedCaption the caption as HTML, or `null` to send it unformatted. + * @param inReplyToEventId the event this message replies to, or `null` if it is not a reply. + */ suspend fun sendGallery( items: List, caption: String?, @@ -165,12 +298,36 @@ interface Timeline : AutoCloseable { inReplyToEventId: EventId?, ): Result + /** + * Redacts an event, i.e. asks the server to remove its content; it also removes a local echo that has not been sent yet. + * + * @param eventOrTransactionId the event to redact, identified by its event id once sent or by its transaction id while still local. + * @param reason the explanation recorded in the redaction event, or `null` for none. + */ suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result + /** + * Adds the given reaction to an event, or removes it if the current user had already reacted with it. + * + * @param emoji the reaction key, usually an emoji. + * @param eventOrTransactionId the event to react to, identified by its event id or by its transaction id while still local. + * @return whether the reaction is now present. + */ suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result + /** + * Forwards the content of an event to other rooms, as new messages sent by the current user. + * + * @param eventId the event whose content is forwarded. + * @param roomIds the rooms to forward it to. + */ suspend fun forwardEvent(eventId: EventId, roomIds: List): Result + /** + * Cancels an event that is still waiting in the send queue, by redacting its local echo. + * + * @param transactionId the transaction id of the local echo to drop. + */ suspend fun cancelSend(transactionId: TransactionId): Result = redactEvent(transactionId.toEventOrTransactionId(), reason = null) @@ -222,12 +379,20 @@ interface Timeline : AutoCloseable { */ suspend fun endPoll(pollStartId: EventId, text: String): Result + /** + * Resolves the content of a replied-to event so it can be rendered above the reply. + * Served from the loaded timeline items when possible, otherwise fetched through the SDK. + * + * @param eventId the event being replied to. + */ suspend fun loadReplyDetails(eventId: EventId): InReplyTo /** * Returns true if [eventId] is currently loaded in this timeline's window, even if the display * layer does not render it (e.g. filtered or state events). This distinguishes "in the window * but not displayed" from "genuinely outside the loaded window". + * + * @param eventId the event to look for. */ suspend fun isEventLoaded(eventId: EventId): Boolean @@ -237,6 +402,8 @@ interface Timeline : AutoCloseable { * * Returns `true` if we sent the request, `false` if the event was already * pinned. + * + * @param eventId the event to pin. */ suspend fun pinEvent(eventId: EventId): Result @@ -246,11 +413,13 @@ interface Timeline : AutoCloseable { * * Returns `true` if we sent the request, `false` if the event wasn't * pinned + * + * @param eventId the event to unpin. */ suspend fun unpinEvent(eventId: EventId): Result /** - * Get the latest event id of the timeline. + * Get the latest event id of the timeline, or `null` when it holds no event yet. */ suspend fun getLatestEventId(): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt index 28e487f527..ef9d5d794e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/TimelineProvider.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.first * By default, the active timeline is the live timeline. */ fun interface TimelineProvider { + /** The timeline currently in use, or `null` while none is available yet; see [getActiveTimeline] to await a non-null value. */ fun activeTimelineFlow(): StateFlow } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt index f3982e83d2..91703f7fa4 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt @@ -60,14 +60,30 @@ data class EventTimelineItem( } } +/** + * Provides the debug information of a timeline item on demand, so the original event JSON is only fetched from the SDK when actually displayed. + */ fun interface TimelineItemDebugInfoProvider { + /** Returns the debug information of the item, including its original and latest edit JSON. */ operator fun invoke(): TimelineItemDebugInfo } +/** + * Provides the encryption warning to show next to a timeline item on demand, so the check is only run for items that are displayed. + */ fun interface MessageShieldProvider { + /** + * Returns the warning to display for this event, or `null` when there is nothing to warn about. + * + * @param strict whether to also report the weaker problems that are hidden by default. + */ operator fun invoke(strict: Boolean): MessageShield? } +/** + * Provides the send queue handle of a timeline item on demand, which is needed to retry or cancel a failed send. + */ fun interface SendHandleProvider { + /** Returns the send handle of the item, or `null` when it is not a local echo waiting in the send queue. */ operator fun invoke(): SendHandle? } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt index d4ef27fd0d..f5de427ce0 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingService.kt @@ -10,8 +10,21 @@ package io.element.android.libraries.matrix.api.tracing import timber.log.Timber +/** + * Routes the app's own logs into the SDK's tracing subsystem, so that Kotlin and Rust logs end up interleaved in the same files. + */ interface TracingService { + /** + * Creates a Timber tree that forwards what the app logs to the SDK tracing subsystem. + * + * @param target the tracing target name the logs are attributed to, used to tell subsystems apart in the output. + */ fun createTimberTree(target: String): Timber.Tree + /** + * Turns writing logs to files on or off at runtime, which is how the user-facing logging toggle is applied. + * + * @param config where to write the log files, or the disabled configuration to stop writing them. + */ fun updateWriteToFilesConfiguration(config: WriteToFilesConfiguration) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt index 38a00d833d..de9b11e882 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt @@ -13,6 +13,12 @@ import io.element.android.libraries.matrix.api.core.UserId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +/** + * Drives session verification: proving to the account that this device is trusted, and verifying other users. + * + * A verification is a stateful exchange between two devices, so at most one flow is active at a time and its progress is reported + * through [verificationFlowState] rather than through the return value of each call. + */ interface SessionVerificationService { /** * State of the current verification flow ([VerificationFlowState.Initial] if not started). @@ -37,6 +43,8 @@ interface SessionVerificationService { /** * Request verification of the user with the given [userId]. + * + * @param userId the other user to verify. */ suspend fun requestUserVerification(userId: UserId) @@ -62,17 +70,23 @@ interface SessionVerificationService { /** * Returns the verification service state to the initial step. + * + * @param cancelAnyPendingVerificationAttempt whether to also cancel a verification that is still in progress. */ suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean) /** * Register a listener to be notified of incoming session verification requests. + * + * @param listener the listener to notify, or `null` to stop being notified. */ fun setListener(listener: SessionVerificationServiceListener?) /** * Set this particular request as the currently active one and register for * events pertaining it. + * + * @param verificationRequest the incoming request to make active. */ suspend fun acknowledgeVerificationRequest(verificationRequest: VerificationRequest.Incoming) @@ -82,7 +96,15 @@ interface SessionVerificationService { suspend fun acceptVerificationRequest() } +/** + * Notified when another device asks to verify this session; register one through [SessionVerificationService.setListener]. + */ interface SessionVerificationServiceListener { + /** + * Called when a verification request is received, so the app can prompt the user about it. + * + * @param verificationRequest the incoming request, to be passed to [SessionVerificationService.acknowledgeVerificationRequest] to act on it. + */ fun onIncomingSessionRequest(verificationRequest: VerificationRequest.Incoming) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallAnalyticCredentialsProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallAnalyticCredentialsProvider.kt index 5b2a691afc..1b45c4f1a0 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallAnalyticCredentialsProvider.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallAnalyticCredentialsProvider.kt @@ -8,10 +8,24 @@ package io.element.android.libraries.matrix.api.widget +/** + * Supplies the analytics and crash reporting credentials passed to the embedded Element Call widget. + * + * Every member is `null` when the corresponding service is not configured for this build, or when the user has not consented to analytics. + */ interface CallAnalyticCredentialsProvider { + /** The PostHog identifier of the current user, so call analytics can be tied to the same person as app analytics. */ val posthogUserId: String? + + /** The PostHog host the widget should send its events to. */ val posthogApiHost: String? + + /** The PostHog API key the widget should use. */ val posthogApiKey: String? + + /** The URL the widget should submit its rageshakes to. */ val rageshakeSubmitUrl: String? + + /** The Sentry DSN the widget should report its crashes to. */ val sentryDsn: String? } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallWidgetSettingsProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallWidgetSettingsProvider.kt index d0686e8e5f..92510c4b8f 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallWidgetSettingsProvider.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallWidgetSettingsProvider.kt @@ -10,7 +10,18 @@ package io.element.android.libraries.matrix.api.widget import java.util.UUID +/** + * Builds the widget settings used to embed Element Call in a room, applying the app's own preferences on top of the SDK defaults. + */ interface CallWidgetSettingsProvider { + /** + * @param baseUrl the URL Element Call is served from. + * @param widgetId a unique identifier for this widget instance. + * @param encrypted whether the room the call takes place in is encrypted. + * @param direct whether the call happens in a direct message room. + * @param isAudioCall whether the call should start without video. + * @param hasActiveCall whether a call is already ongoing in the room, which affects whether the user joins straight away. + */ suspend fun provide( baseUrl: String, widgetId: String = UUID.randomUUID().toString(), diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetDriver.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetDriver.kt index d8cca95842..3b863279cf 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetDriver.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/MatrixWidgetDriver.kt @@ -10,10 +10,25 @@ package io.element.android.libraries.matrix.api.widget import kotlinx.coroutines.flow.Flow +/** + * Bridges a widget running in a WebView with the Matrix room it is embedded in, relaying the widget API messages in both directions. + * + * The driver owns SDK resources and must be closed once the widget goes away; Element Call is the main user of this. + */ interface MatrixWidgetDriver : AutoCloseable { + /** The id of the widget this driver serves, matching the one in its settings. */ val id: String + + /** The messages coming from the room that should be forwarded to the widget. */ val incomingMessages: Flow + /** Starts relaying messages and suspends for as long as the driver runs; calling it again while it is running does nothing. */ suspend fun run() + + /** + * Forwards a message from the widget to the room. Does nothing once the driver has been closed. + * + * @param message the raw widget API message emitted by the WebView. + */ suspend fun send(message: String) } diff --git a/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt b/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt index 62b169744b..228fbd2cff 100644 --- a/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt +++ b/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderHolder.kt @@ -11,8 +11,24 @@ import coil3.ImageLoader import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId +/** + * Caches one Coil image loader per session, since loading Matrix media requires the session's own client to fetch and decrypt it. + */ interface ImageLoaderHolder { + /** Returns a loader that can only fetch ordinary URLs, for images that do not come from a Matrix room. */ fun get(): ImageLoader + + /** + * Returns the loader of a session, creating it on first use. + * + * @param client the session whose media the loader will fetch. + */ fun get(client: MatrixClient): ImageLoader + + /** + * Drops the cached loader of a session, to be called when that session goes away. + * + * @param sessionId the session whose loader is dropped. + */ fun remove(sessionId: SessionId) } diff --git a/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/InitialsAvatarBitmapGenerator.kt b/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/InitialsAvatarBitmapGenerator.kt index e6ff5d762d..f145383e87 100644 --- a/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/InitialsAvatarBitmapGenerator.kt +++ b/libraries/matrixmedia/api/src/main/kotlin/io/element/android/libraries/matrix/ui/media/InitialsAvatarBitmapGenerator.kt @@ -14,6 +14,15 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData * Generates a bitmap for an initials avatar based on the provided [io.element.android.libraries.designsystem.components.avatar.AvatarData]. */ interface InitialsAvatarBitmapGenerator { + /** + * Draws the initials avatar, used where a bitmap is required rather than a Composable, such as in notifications and shortcuts. + * + * @param size the width and height of the bitmap in pixels. + * @param avatarData the name and id the initials and the background colour are derived from. + * @param useDarkTheme whether to draw the dark theme variant. + * @param fontSizePercentage the text height as a fraction of [size]. + * @return the bitmap, or `null` when it could not be drawn. + */ fun generateBitmap( size: Int, avatarData: AvatarData, diff --git a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerProvider.kt b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerProvider.kt index 1bab693609..185b2f58a0 100644 --- a/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerProvider.kt +++ b/libraries/mediapickers/api/src/main/kotlin/io/element/android/libraries/mediapickers/api/PickerProvider.kt @@ -12,37 +12,79 @@ import android.net.Uri import androidx.activity.result.PickVisualMediaRequest import androidx.compose.runtime.Composable +/** + * Registers the system media and file pickers from Composable code, returning launchers the UI can trigger later. + * + * Every method must be called during composition, not from a click handler; a cancelled pick reports a `null` uri or an empty list. + */ interface PickerProvider { + /** + * Registers a picker for a single photo or video from the gallery. + * + * @param onResult called with the picked media and its MIME type, both `null` when the user cancelled. + */ @Composable fun registerGalleryPicker( onResult: (uri: Uri?, mimeType: String?) -> Unit ): PickerLauncher + /** + * Registers a picker restricted to a single image, used where a video would not be valid, such as for an avatar. + * + * @param onResult called with the picked image, `null` when the user cancelled. + */ @Composable fun registerGalleryImagePicker( onResult: (Uri?) -> Unit ): PickerLauncher + /** + * Registers a picker for several photos or videos at once. + * + * @param onResult called with the picked media, empty when the user cancelled. + */ @Composable fun registerGalleryMultiPicker( onResult: (uris: List) -> Unit ): PickerLauncher> + /** + * Registers a picker for a single file of any kind. + * + * @param mimeType the MIME type to filter on, `*` / `*` to accept anything. + * @param onResult called with the picked file and its MIME type, both `null` when the user cancelled. + */ @Composable fun registerFilePicker( mimeType: String, onResult: (uri: Uri?, mimeType: String?) -> Unit, ): PickerLauncher + /** + * Registers a picker for several files at once. + * + * @param mimeType the MIME type to filter on. + * @param onResult called with the picked files, empty when the user cancelled. + */ @Composable fun registerFileMultiPicker( mimeType: String, onResult: (uris: List) -> Unit, ): PickerLauncher, List> + /** + * Registers a launcher that opens the camera to take a photo. + * + * @param onResult called with the captured photo, `null` when the user cancelled or the capture failed. + */ @Composable fun registerCameraPhotoPicker(onResult: (Uri?) -> Unit): PickerLauncher + /** + * Registers a launcher that opens the camera to record a video. + * + * @param onResult called with the recorded video, `null` when the user cancelled or the capture failed. + */ @Composable fun registerCameraVideoPicker(onResult: (Uri?) -> Unit): PickerLauncher } diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MaxUploadSizeProvider.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MaxUploadSizeProvider.kt index a5462a6caf..e6b9c7f026 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MaxUploadSizeProvider.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MaxUploadSizeProvider.kt @@ -12,5 +12,6 @@ package io.element.android.libraries.mediaupload.api * Provides the maximum upload size allowed by the Matrix server. */ fun interface MaxUploadSizeProvider { + /** Returns the maximum size in bytes a single upload may have, as advertised by the homeserver. */ suspend fun getMaxUploadSize(): Result } diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaOptimizationConfigProvider.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaOptimizationConfigProvider.kt index 976a7057f2..60dbf353eb 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaOptimizationConfigProvider.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaOptimizationConfigProvider.kt @@ -8,6 +8,10 @@ package io.element.android.libraries.mediaupload.api +/** + * Resolves how aggressively media should be compressed, combining the user's own preferences with the app defaults. + */ fun interface MediaOptimizationConfigProvider { + /** Returns the configuration to pass to the pre-processing and sending calls. */ suspend fun get(): MediaOptimizationConfig } diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt index c2bf1e5f5d..a51ebc17b9 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt @@ -10,10 +10,18 @@ package io.element.android.libraries.mediaupload.api import android.net.Uri +/** + * Prepares a media file for upload: resizing, transcoding and stripping metadata that would leak information about the sender. + */ interface MediaPreProcessor { /** * Given a [uri] and [mimeType], pre-processes the media before it's uploaded, resizing, transcoding, and removing sensitive info from its metadata. * If [deleteOriginal] is `true`, the file reference by the [uri] will be automatically deleted too when this process finishes. + * + * @param uri the media to process. + * @param mimeType the MIME type of that media. + * @param deleteOriginal whether to delete the source file once processing is done. + * @param mediaOptimizationConfig how aggressively the media should be compressed. * @return a [Result] with the [MediaUploadInfo] containing all the info needed to begin the upload. */ suspend fun process( diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt index 6cca73935e..808575f9e6 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -13,31 +13,62 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.timeline.Timeline +/** + * Creates a [MediaSender] for the room of the current room scope; see [MediaSenderRoomFactory] when the room is not in scope. + */ fun interface MediaSenderFactory { /** * Create a [MediaSender] for the given [Timeline.Mode], in the Room Scope. + * + * @param timelineMode the timeline the media should be sent to, which matters for threads in particular. */ fun create( timelineMode: Timeline.Mode, ): MediaSender } +/** + * Creates a [MediaSender] for an explicitly given room, used outside the room scope, for instance when sharing into a room. + */ fun interface MediaSenderRoomFactory { /** * Create a [MediaSender] for the given [JoinedRoom], with timeline mode Live. + * + * @param room the room the media should be sent to. */ fun create( room: JoinedRoom, ): MediaSender } +/** + * Sends media to a room, taking care of the pre-processing the timeline API does not do: resizing, transcoding and stripping metadata. + * + * Sending is a two-step process, and the steps can be used separately so the UI can show a preview between them. + * The temporary files produced by pre-processing are only released by [cleanUp]. + */ interface MediaSender { + /** + * Resizes, transcodes and strips the metadata of a media file without sending anything yet. + * + * @param uri the media picked by the user. + * @param mimeType the MIME type of that media. + * @param mediaOptimizationConfig how aggressively the media should be compressed. + */ suspend fun preProcessMedia( uri: Uri, mimeType: String, mediaOptimizationConfig: MediaOptimizationConfig, ): Result + /** + * Sends media that has already gone through [preProcessMedia]. + * + * @param mediaUploadInfo the result of the pre-processing step. + * @param caption the text shown along with the media, or `null` for none. + * @param formattedCaption the caption as HTML, or `null` to send it unformatted. + * @param inReplyToEventId the event this message replies to, or `null` if it is not a reply. + */ suspend fun sendPreProcessedMedia( mediaUploadInfo: MediaUploadInfo, caption: String?, @@ -45,6 +76,16 @@ interface MediaSender { inReplyToEventId: EventId?, ): Result + /** + * Pre-processes and sends a media file in one go, suspending until the upload finishes. + * + * @param uri the media picked by the user. + * @param mimeType the MIME type of that media. + * @param caption the text shown along with the media, or `null` for none. + * @param formattedCaption the caption as HTML, or `null` to send it unformatted. + * @param inReplyToEventId the event this message replies to, or `null` if it is not a reply. + * @param mediaOptimizationConfig how aggressively the media should be compressed. + */ suspend fun sendMedia( uri: Uri, mimeType: String, @@ -54,6 +95,14 @@ interface MediaSender { mediaOptimizationConfig: MediaOptimizationConfig, ): Result + /** + * Sends a recorded voice message, which is never compressed and carries a waveform instead of a caption. + * + * @param uri the recorded audio file. + * @param mimeType the MIME type of that recording. + * @param waveForm the amplitudes used to draw the waveform. + * @param inReplyToEventId the event this message replies to, or `null` if it is not a reply. + */ suspend fun sendVoiceMessage( uri: Uri, mimeType: String, @@ -61,6 +110,14 @@ interface MediaSender { inReplyToEventId: EventId? = null, ): Result + /** + * Sends several already pre-processed media items as a single gallery message. + * + * @param mediaUploadInfos the pre-processed items, in the order they should appear. + * @param caption the text shown along with the gallery, or `null` for none. + * @param formattedCaption the caption as HTML, or `null` to send it unformatted. + * @param inReplyToEventId the event this message replies to, or `null` if it is not a reply. + */ suspend fun sendGallery( mediaUploadInfos: List, caption: String?, @@ -68,5 +125,6 @@ interface MediaSender { inReplyToEventId: EventId?, ): Result + /** Deletes the temporary files produced by pre-processing; must be called even when nothing was ultimately sent. */ fun cleanUp() } diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaFactory.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaFactory.kt index 383f985c4f..5a218f9e1e 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaFactory.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaFactory.kt @@ -12,9 +12,15 @@ import android.net.Uri import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.mediaviewer.api.MediaInfo +/** + * Builds the [LocalMedia] the media viewer renders, from either a downloaded Matrix media file or an arbitrary content uri. + */ interface LocalMediaFactory { /** * This method will create a [LocalMedia] with the given [MediaFile] and [MediaInfo]. + * + * @param mediaFile the file already downloaded from the homeserver. + * @param mediaInfo what is known about the media from the event that carried it. */ fun createFromMediaFile( mediaFile: MediaFile, @@ -24,6 +30,11 @@ interface LocalMediaFactory { /** * This method will create a [LocalMedia] with the given mimeType, name and formattedFileSize * If any of those params are null, it'll try to read them from the content. + * + * @param uri the content to display, for instance one just picked by the user. + * @param mimeType the MIME type, or `null` to read it from the content. + * @param name the file name, or `null` to read it from the content. + * @param formattedFileSize the size to display, or `null` to read it from the content. */ fun createFromUri( uri: Uri, diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaRenderer.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaRenderer.kt index fb7f26101e..5bc84919be 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaRenderer.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaRenderer.kt @@ -10,7 +10,15 @@ package io.element.android.libraries.mediaviewer.api.local import androidx.compose.runtime.Composable +/** + * Renders a media file held on the device, picking the right player or viewer for its type. + */ interface LocalMediaRenderer { + /** + * Draws the media, filling the space it is given. + * + * @param localMedia the file to display, along with what is known about its type. + */ @Composable fun Render(localMedia: LocalMedia) } diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractor.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractor.kt index 094b4513d8..e29188e984 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractor.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/util/FileExtensionExtractor.kt @@ -8,6 +8,13 @@ package io.element.android.libraries.mediaviewer.api.util +/** + * Extracts the extension of a file name, so it can be shown to the user and used to pick an icon. + */ interface FileExtensionExtractor { + /** + * @param name the file name to inspect. + * @return the extension without its dot, or an empty string when the name has none. + */ fun extractFromName(name: String): String } diff --git a/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthActionFlow.kt b/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthActionFlow.kt index c6791e3197..7cb35de176 100644 --- a/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthActionFlow.kt +++ b/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthActionFlow.kt @@ -10,8 +10,26 @@ package io.element.android.libraries.oauth.api import kotlinx.coroutines.flow.FlowCollector +/** + * Carries the OAuth redirect from the activity that receives it to the login screen waiting for it. + * + * The two are not in the same navigation subtree, so the result cannot simply be returned. + */ interface OAuthActionFlow { + /** + * Publishes the action just received, to be picked up by whoever is collecting. + * + * @param oAuthAction the action resolved from the redirect intent. + */ fun post(oAuthAction: OAuthAction) + + /** + * Observes the published actions; `null` is emitted when there is nothing pending. + * + * @param collector the collector to feed. + */ suspend fun collect(collector: FlowCollector) + + /** Clears the pending action, so it is not handled twice. */ fun reset() } diff --git a/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthIntentResolver.kt b/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthIntentResolver.kt index 2091a86db4..5187649298 100644 --- a/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthIntentResolver.kt +++ b/libraries/oauth/api/src/main/kotlin/io/element/android/libraries/oauth/api/OAuthIntentResolver.kt @@ -10,6 +10,13 @@ package io.element.android.libraries.oauth.api import android.content.Intent +/** + * Recognises the redirect the authentication server sends the user back with once they have signed in or cancelled. + */ interface OAuthIntentResolver { + /** + * @param intent the intent the app was resumed with. + * @return the action to carry out, or `null` when the intent is not an OAuth redirect. + */ fun resolve(intent: Intent): OAuthAction? } diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionStateProvider.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionStateProvider.kt index beeef0b234..98fb3edbe0 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionStateProvider.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionStateProvider.kt @@ -10,11 +10,40 @@ package io.element.android.libraries.permissions.api import kotlinx.coroutines.flow.Flow +/** + * Answers permission questions by combining the live system state with what [PermissionsStore] remembers about past requests. + */ interface PermissionStateProvider { + /** + * Whether the permission is granted right now, read from the system rather than from the store. + * + * @param permission the Android permission name. + */ fun isPermissionGranted(permission: String): Boolean + + /** + * @param permission the Android permission name. + * @param value true once the user has refused this permission. + */ suspend fun setPermissionDenied(permission: String, value: Boolean) + + /** + * Whether the user has refused this permission before. + * + * @param permission the Android permission name. + */ fun isPermissionDenied(permission: String): Flow + /** + * @param permission the Android permission name. + * @param value true once the system dialog for this permission has been shown. + */ suspend fun setPermissionAsked(permission: String, value: Boolean) + + /** + * Whether the system dialog for this permission has already been shown. + * + * @param permission the Android permission name. + */ fun isPermissionAsked(permission: String): Flow } diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsPresenter.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsPresenter.kt index f2ccb2bfc3..07918d0c86 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsPresenter.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsPresenter.kt @@ -11,7 +11,13 @@ package io.element.android.libraries.permissions.api import io.element.android.libraries.architecture.Presenter interface PermissionsPresenter : Presenter { + /** + * Creates a presenter dedicated to one permission. + */ interface Factory { + /** + * @param permission the Android permission name this presenter will request. + */ fun create(permission: String): PermissionsPresenter } } diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStore.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStore.kt index 3bb7d2c679..2d37f016f9 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStore.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStore.kt @@ -10,15 +10,44 @@ package io.element.android.libraries.permissions.api import kotlinx.coroutines.flow.Flow +/** + * Remembers what the app has already asked the user about, since Android itself does not let an app tell + * "never asked" apart from "asked and denied". + */ interface PermissionsStore { + /** + * @param permission the Android permission name. + * @param value true once the user has refused this permission. + */ suspend fun setPermissionDenied(permission: String, value: Boolean) + + /** + * Whether the user has refused this permission before. + * + * @param permission the Android permission name. + */ fun isPermissionDenied(permission: String): Flow + /** + * @param permission the Android permission name. + * @param value true once the system dialog for this permission has been shown. + */ suspend fun setPermissionAsked(permission: String, value: Boolean) + + /** + * Whether the system dialog for this permission has already been shown, which decides whether the app may show a rationale instead. + * + * @param permission the Android permission name. + */ fun isPermissionAsked(permission: String): Flow + /** + * Forgets what was recorded about one permission, so the app behaves as if it had never been requested. + * + * @param permission the Android permission name. + */ suspend fun resetPermission(permission: String) - // To debug + /** Forgets every recorded permission; exposed for debugging from the developer options. */ suspend fun resetStore() } diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionAdvisor.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionAdvisor.kt index 9863dbb751..8d801cfe67 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionAdvisor.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionAdvisor.kt @@ -7,10 +7,15 @@ package io.element.android.libraries.permissions.api.localnetwork +/** + * Decides whether the local network permission is needed to reach a homeserver, which is the case for a server on the user's own network. + */ interface LocalNetworkPermissionAdvisor { /** * Returns true when the app should request the ACCESS_LOCAL_NETWORK permission before making * network requests to [homeserverUrl]. + * + * @param homeserverUrl the server the app is about to contact. */ suspend fun shouldRequestPermissionFor(homeserverUrl: String): Boolean } diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt index b1877b3ae5..f35fa88442 100644 --- a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt @@ -13,48 +13,111 @@ import io.element.android.libraries.matrix.api.tracing.LogLevel import io.element.android.libraries.matrix.api.tracing.TraceLogPack import kotlinx.coroutines.flow.Flow +/** + * Local, app-wide user preferences that are not tied to a session, stored on the device only. + * + * Each getter returns a flow that emits the current value straight away and then again on every change. + * The default each preference falls back to when it has never been set is given on the getter. + */ interface AppPreferencesStore { + /** + * @param enabled true to expose the developer options in the settings. + */ suspend fun setDeveloperModeEnabled(enabled: Boolean) + + /** Whether developer mode is on; defaults to `true` on debug builds and `false` otherwise. */ fun isDeveloperModeEnabledFlow(): Flow + /** + * @param string the Element Call deployment to use, or `null` to go back to the one from the homeserver. + */ suspend fun setCustomElementCallBaseUrl(string: String?) + + /** The Element Call URL the user has overridden, or `null` when the homeserver's own value should be used. */ fun getCustomElementCallBaseUrlFlow(): Flow + /** + * @param theme the name of the theme to apply: light, dark, or follow the system. + */ suspend fun setTheme(theme: String) + + /** The name of the chosen theme, or `null` when the user has not picked one and the system setting should be followed. */ fun getThemeFlow(): Flow + /** + * @param value the distance in metres the user must move before a new live location is published. + */ suspend fun setLiveLocationMinimumDistanceInMetersUpdate(value: Int) + + /** The minimum distance in metres between two live location updates; defaults to 10. */ fun getLiveLocationMinimumDistanceInMetersUpdateFlow(): Flow + /** + * Only used to clear the local value once it has been migrated to the server. + * + * @param hide the local setting, or `null` to erase it. + */ @Deprecated("Use MediaPreviewService instead. Kept only for migration.") suspend fun setHideInviteAvatars(hide: Boolean?) + + /** The local setting to migrate to the server, or `null` once there is nothing left to migrate. */ @Deprecated("Use MediaPreviewService instead. Kept only for migration.") fun getHideInviteAvatarsFlow(): Flow + + /** + * Only used to clear the local value once it has been migrated to the server. + * + * @param mediaPreviewValue the local setting, or `null` to erase it. + */ @Deprecated("Use MediaPreviewService instead. Kept only for migration.") suspend fun setTimelineMediaPreviewValue(mediaPreviewValue: MediaPreviewValue?) + + /** The local setting to migrate to the server, or `null` once there is nothing left to migrate. */ @Deprecated("Use MediaPreviewService instead. Kept only for migration.") fun getTimelineMediaPreviewValueFlow(): Flow + /** + * @param logLevel how verbose the SDK and app logs should be. + */ suspend fun setTracingLogLevel(logLevel: LogLevel) + + /** The configured log level; defaults to the one the build was compiled with. */ fun getTracingLogLevelFlow(): Flow + /** + * @param targets the extra log packs to enable on top of the default targets. + */ suspend fun setTracingLogPacks(targets: Set) + + /** The extra log packs enabled by the user; defaults to an empty set. */ fun getTracingLogPacksFlow(): Flow> + /** The sound played for message notifications. */ fun getMessageSoundFlow(): Flow /** * Atomically persists [sound] (with copy-time [title] for Custom; cleared otherwise) and * bumps the channel version. Single transaction so process death can't desync URI and version. + * + * @param sound the sound to persist. + * @param title the display name captured when the sound was copied, or `null` for the built-in choices. + * @return the new channel version, to be passed to the channel recreation. */ suspend fun setMessageSoundAndIncrementVersion(sound: NotificationSound, title: String?): Int /** Title captured at copy time. Null for SystemDefault / Silent or pre-title persisted data. */ fun getMessageSoundDisplayNameFlow(): Flow + /** The sound played for incoming call notifications. */ fun getCallRingtoneFlow(): Flow - /** See [setMessageSoundAndIncrementVersion]. */ + /** + * See [setMessageSoundAndIncrementVersion]. + * + * @param sound the ringtone to persist. + * @param title the display name captured when the sound was copied, or `null` for the built-in choices. + * @return the new channel version, to be passed to the channel recreation. + */ suspend fun setCallRingtoneAndIncrementVersion(sound: NotificationSound, title: String?): Int /** See [getMessageSoundDisplayNameFlow]. */ @@ -63,5 +126,6 @@ interface AppPreferencesStore { /** Single-snapshot read of all sound prefs; used at boot to seed channels without N reads. */ suspend fun getNotificationSoundChannelConfig(): NotificationSoundChannelConfig + /** Erases every app preference, so they all fall back to their defaults. */ suspend fun reset() } diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/PreferenceDataStoreFactory.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/PreferenceDataStoreFactory.kt index 50f6eb2431..a3d609e205 100644 --- a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/PreferenceDataStoreFactory.kt +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/PreferenceDataStoreFactory.kt @@ -17,5 +17,10 @@ import androidx.datastore.preferences.core.Preferences * It's a wrapper around AndroidX's `PreferenceDataStoreFactory` to make testing easier. */ interface PreferenceDataStoreFactory { + /** + * Creates or opens the preference store with the given name. + * + * @param name the store name, which also determines the file it is persisted to. + */ fun create(name: String): DataStore } diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/SessionPreferencesStore.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/SessionPreferencesStore.kt index d3f3fb4e28..b554fff460 100644 --- a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/SessionPreferencesStore.kt +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/SessionPreferencesStore.kt @@ -10,30 +10,77 @@ package io.element.android.libraries.preferences.api.store import kotlinx.coroutines.flow.Flow +/** + * Local, per-session user preferences, stored on the device only and never synced to the homeserver. + * + * Each getter returns a flow that emits the current value straight away and then again on every change. + * The default each preference falls back to when it has never been set is given on the getter. + */ interface SessionPreferencesStore { + /** + * @param enabled true to let the homeserver share this user's presence with others. + */ suspend fun setSharePresence(enabled: Boolean) + + /** Whether presence is shared; defaults to `true`. */ fun isSharePresenceEnabled(): Flow + /** + * @param enabled true to send read receipts that other members can see. + */ suspend fun setSendPublicReadReceipts(enabled: Boolean) + + /** Whether read receipts are sent publicly rather than privately; defaults to `true`. */ fun isSendPublicReadReceiptsEnabled(): Flow + /** + * @param enabled true to display the read receipts of other members in the timeline. + */ suspend fun setRenderReadReceipts(enabled: Boolean) + + /** Whether other members' read receipts are displayed; defaults to `true`. */ fun isRenderReadReceiptsEnabled(): Flow + /** + * @param enabled true to tell other members when this user is typing. + */ suspend fun setSendTypingNotifications(enabled: Boolean) + + /** Whether typing notifications are sent; defaults to `true`. */ fun isSendTypingNotificationsEnabled(): Flow + /** + * @param enabled true to display which other members are typing. + */ suspend fun setRenderTypingNotifications(enabled: Boolean) + + /** Whether other members' typing notifications are displayed; defaults to `true`. */ fun isRenderTypingNotificationsEnabled(): Flow + /** + * @param skip true to remember that the user dismissed the session verification prompt. + */ suspend fun setSkipSessionVerification(skip: Boolean) + + /** Whether the user chose to skip verifying this session, so they are not asked again; defaults to `false`. */ fun isSessionVerificationSkipped(): Flow + /** + * @param compress true to downscale images before uploading them. + */ suspend fun setOptimizeImages(compress: Boolean) + + /** Whether images are downscaled before upload; defaults to `true`. */ fun doesOptimizeImages(): Flow + /** + * @param preset the quality and size trade-off to apply when compressing videos. + */ suspend fun setVideoCompressionPreset(preset: VideoCompressionPreset) + + /** The video compression preset; defaults to [VideoCompressionPreset.STANDARD], including when the stored value is unreadable. */ fun getVideoCompressionPreset(): Flow + /** Erases every preference of this session, so they all fall back to their defaults. */ suspend fun clear() } diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/SessionPreferencesStoreFactory.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/SessionPreferencesStoreFactory.kt index 2c0038d06e..05b4845026 100644 --- a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/SessionPreferencesStoreFactory.kt +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/SessionPreferencesStoreFactory.kt @@ -11,7 +11,22 @@ package io.element.android.libraries.preferences.api.store import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.coroutines.CoroutineScope +/** + * Caches one [SessionPreferencesStore] per session, so that every caller of a given session shares the same instance. + */ interface SessionPreferencesStoreFactory { + /** + * Returns the store of a session, creating it on first use. + * + * @param sessionId the session whose store is requested. + * @param sessionCoroutineScope the scope the store uses for its own work, only honoured when the store is actually created. + */ fun get(sessionId: SessionId, sessionCoroutineScope: CoroutineScope): SessionPreferencesStore + + /** + * Drops the cached store of a session, to be called when that session goes away. + * + * @param sessionId the session whose store is dropped. + */ fun remove(sessionId: SessionId) } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/GetCurrentPushProvider.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/GetCurrentPushProvider.kt index a6fbb47173..eb2ce09545 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/GetCurrentPushProvider.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/GetCurrentPushProvider.kt @@ -10,6 +10,16 @@ package io.element.android.libraries.push.api import io.element.android.libraries.matrix.api.core.SessionId +/** + * Reads the name of the push provider a session is currently registered with. + * + * This is the low-level accessor used where depending on the whole push feature would be too much; prefer [PushService.getCurrentPushProvider] otherwise. + */ interface GetCurrentPushProvider { + /** + * Returns the stored provider name, or `null` when the session has no push provider selected yet. + * + * @param sessionId the session to read the provider of. + */ suspend fun getCurrentPushProvider(sessionId: SessionId): String? } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt index a43ee36403..4a5037d17f 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt @@ -16,9 +16,14 @@ import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.PushProvider import kotlinx.coroutines.flow.Flow +/** + * Entry point for push notification setup: choosing a push provider, registering a pusher with the homeserver, and inspecting the push history. + */ interface PushService { /** * Return the current push provider, or null if none. + * + * @param sessionId the session to read the provider of. */ suspend fun getCurrentPushProvider(sessionId: SessionId): PushProvider? @@ -31,6 +36,10 @@ interface PushService { * Will unregister any previous pusher and register a new one with the provided [PushProvider]. * * The method has effect only if the [PushProvider] is different than the current one. + * + * @param matrixClient the session to register the pusher for. + * @param pushProvider the provider to switch to. + * @param distributor the distributor of that provider to use, which matters for providers such as UnifiedPush. */ suspend fun registerWith( matrixClient: MatrixClient, @@ -42,6 +51,8 @@ interface PushService { * Ensure that the pusher with the current push provider and distributor is registered. * If there is no current config, the default push provider with the default distributor will be used. * Error can be [PusherRegistrationFailure]. + * + * @param matrixClient the session to ensure the pusher of. */ suspend fun ensurePusherIsRegistered( matrixClient: MatrixClient, @@ -50,17 +61,35 @@ interface PushService { /** * Store the given push provider as the current one, but do not register. * To be used when there is no distributor available. + * + * @param sessionId the session to store the provider for. + * @param pushProvider the provider to remember. */ suspend fun selectPushProvider( sessionId: SessionId, pushProvider: PushProvider, ) + /** + * Whether the user has asked not to be warned about pusher registration failures for this session. + * + * @param sessionId the session to read the preference of. + */ fun ignoreRegistrationError(sessionId: SessionId): Flow + + /** + * Remembers whether pusher registration failures should be reported to the user for this session. + * + * @param sessionId the session to store the preference for. + * @param ignore true to stop warning the user about registration failures. + */ suspend fun setIgnoreRegistrationError(sessionId: SessionId, ignore: Boolean) /** + * Asks the homeserver to send a test push, so the user can check that the whole chain works. * Return false in case of early error. + * + * @param sessionId the session to test the push setup of. */ suspend fun testPush(sessionId: SessionId): Boolean @@ -86,6 +115,8 @@ interface PushService { /** * Notify the user that the service is un-registered. + * + * @param userId the user whose push service is no longer registered. */ suspend fun onServiceUnregistered(userId: UserId) } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt index bded69991b..9dd53ade09 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationBitmapLoader.kt @@ -14,6 +14,9 @@ import coil3.ImageLoader import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.ui.media.AVATAR_THUMBNAIL_SIZE_IN_PIXEL +/** + * Loads the avatars used to decorate notifications, since the notification APIs need bitmaps rather than Compose images. + */ interface NotificationBitmapLoader { /** * Get icon of a room. diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationCleaner.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationCleaner.kt index 0a4e35c697..95966f8eb1 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationCleaner.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationCleaner.kt @@ -13,12 +13,56 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId +/** + * Dismisses notifications that are already displayed, typically because the user has now seen the events elsewhere. + * + * Every method also removes the grouping summary notification when it is left without children. + */ interface NotificationCleaner { + /** + * Dismisses every message notification of a session. + * + * @param sessionId the session whose notifications are dismissed. + */ fun clearAllMessagesEvents(sessionId: SessionId) + + /** + * Dismisses the message notifications of one room. + * + * @param sessionId the session the room belongs to. + * @param roomId the room whose notifications are dismissed. + */ fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) + + /** + * Dismisses the message notifications of one thread, leaving the rest of the room's notifications in place. + * + * @param sessionId the session the room belongs to. + * @param roomId the room the thread belongs to. + * @param threadId the thread whose notifications are dismissed. + */ fun clearMessagesForThread(sessionId: SessionId, roomId: RoomId, threadId: ThreadId) + + /** + * Dismisses the notification of a single event. + * + * @param sessionId the session the event belongs to. + * @param eventId the event whose notification is dismissed. + */ fun clearEvent(sessionId: SessionId, eventId: EventId) + /** + * Dismisses every membership notification of a session, i.e. the invitations and the join requests. + * + * @param sessionId the session whose notifications are dismissed. + */ fun clearMembershipNotificationForSession(sessionId: SessionId) + + /** + * Dismisses the membership notifications of one room. + * + * @param sessionId the session the room belongs to. + * @param roomId the room whose notifications are dismissed. + */ fun clearMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId) } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationSoundUpdater.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationSoundUpdater.kt index b23c4122ad..91f50abc07 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationSoundUpdater.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/NotificationSoundUpdater.kt @@ -14,8 +14,20 @@ import io.element.android.libraries.preferences.api.store.NotificationSound * versioned channel because Android forbids mutating sound after creation. */ interface NotificationSoundUpdater { + /** + * Recreates the message notification channel so it uses [sound]. + * + * @param sound the sound the new channel should play. + * @param version bumped on every change, since a new channel id is the only way to change a channel's sound. + */ fun recreateNoisyChannel(sound: NotificationSound, version: Int) + /** + * Recreates the ringing call notification channel so it uses [sound]; see [recreateNoisyChannel] for why a version is needed. + * + * @param sound the sound the new channel should play. + * @param version bumped on every change to force a new channel id. + */ fun recreateRingingCallChannel(sound: NotificationSound, version: Int) /** Current channel sound classified into [NotificationSound]. Null when the channel doesn't exist. */ diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/OnMissedCallNotificationHandler.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/OnMissedCallNotificationHandler.kt index 2ba61925c5..d4e051ebf2 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/OnMissedCallNotificationHandler.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/OnMissedCallNotificationHandler.kt @@ -18,6 +18,10 @@ import io.element.android.libraries.matrix.api.core.SessionId interface OnMissedCallNotificationHandler { /** * Adds a missed call notification. + * + * @param sessionId the session the call was received on. + * @param roomId the room the call took place in. + * @param eventId the call notification event that went unanswered. */ suspend fun addMissedCallNotification( sessionId: SessionId, diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/conversations/NotificationConversationService.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/conversations/NotificationConversationService.kt index 4ea80e4ce8..b3bffc0147 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/conversations/NotificationConversationService.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/conversations/NotificationConversationService.kt @@ -18,6 +18,12 @@ interface NotificationConversationService { /** * Called when a new message is received in a room. * It should create a new conversation shortcut for this room. + * + * @param sessionId the session the message belongs to. + * @param roomId the room the message was received in. + * @param roomName the name to show on the shortcut, or `null` when the room has none. + * @param roomIsDirect whether the room is a direct message, which changes how the shortcut is presented. + * @param roomAvatarUrl the avatar to show on the shortcut, or `null` when the room has none. */ suspend fun onSendMessage( sessionId: SessionId, @@ -30,12 +36,18 @@ interface NotificationConversationService { /** * Called when a room is left. * It should remove the conversation shortcut for this room. + * + * @param sessionId the session the room belongs to. + * @param roomId the room that was left. */ suspend fun onLeftRoom(sessionId: SessionId, roomId: RoomId) /** * Called when the list of available rooms changes. * It should update the conversation shortcuts accordingly, removing shortcuts for no longer joined rooms. + * + * @param sessionId the session the rooms belong to. + * @param roomIds the rooms the user is currently joined to. */ suspend fun onAvailableRoomsChanged(sessionId: SessionId, roomIds: Set) } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/sound/NotificationSoundCopier.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/sound/NotificationSoundCopier.kt index 2268198353..ad3d73656f 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/sound/NotificationSoundCopier.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/notifications/sound/NotificationSoundCopier.kt @@ -12,6 +12,12 @@ package io.element.android.libraries.push.api.notifications.sound * FileProvider, so the persisted reference outlives the original source. */ interface NotificationSoundCopier { + /** + * Copies the picked sound into app-private storage, replacing any file previously stored for [slot]. + * + * @param sourceUriString the uri of the sound the user picked. + * @param slot which of the two sounds is being set, since each keeps its own copy. + */ suspend fun copyToAppFiles(sourceUriString: String, slot: SoundSlot): CopyResult /** diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushHandler.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushHandler.kt index 49b578070b..1e53575717 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushHandler.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushHandler.kt @@ -8,6 +8,9 @@ package io.element.android.libraries.pushproviders.api +/** + * Entry point for the pushes coming from any provider, which resolves them into notifications. + */ interface PushHandler { /** * Handle a push received from the provider. @@ -22,7 +25,10 @@ interface PushHandler { ): Boolean /** - * Handle an invalid push received from the provider. + * Handle an invalid push received from the provider, which is recorded in the push history so it can be diagnosed. + * + * @param providerInfo an identifier of the provider that sent the push, for logging and debugging purposes. + * @param data the raw payload that could not be understood. */ suspend fun handleInvalid( providerInfo: String, diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushProvider.kt index 405a83b9b8..ed13d0ab37 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushProvider.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PushProvider.kt @@ -37,33 +37,54 @@ interface PushProvider { /** * Register the pusher to the homeserver. + * + * @param matrixClient the session to register the pusher for. + * @param distributor the distributor to route the notifications through. */ suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor): Result /** - * Return the current distributor, or null if none. + * Return the current distributor as its raw stored value, or null if none. + * + * @param sessionId the session to read the distributor of. */ suspend fun getCurrentDistributorValue(sessionId: SessionId): String? /** - * Return the current distributor, or null if none. + * Return the current distributor, or null if none, which also happens when the stored one is no longer installed. + * + * @param sessionId the session to read the distributor of. */ suspend fun getCurrentDistributor(sessionId: SessionId): Distributor? /** * Unregister the pusher. + * + * @param matrixClient the session to unregister the pusher of. */ suspend fun unregister(matrixClient: MatrixClient): Result /** * To invoke when the session is deleted. + * + * @param sessionId the session that has been removed, whose provider-side data should be cleaned up. */ suspend fun onSessionDeleted(sessionId: SessionId) + /** + * The gateway and push key currently in use, for display in the troubleshooting screens; `null` when not registered. + * + * @param sessionId the session to read the configuration of. + */ suspend fun getPushConfig(sessionId: SessionId): Config? + /** Whether this provider can renew its push token on demand, which not every provider supports. */ fun canRotateToken(): Boolean + /** + * Renews the push token, which is one of the recovery steps offered when notifications stop arriving. + * Throws when [canRotateToken] is `false`. + */ suspend fun rotateToken(): Result { error("rotateToken() not implemented, you need to override this method in your implementation") } diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PusherSubscriber.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PusherSubscriber.kt index 989170c975..e1d885fca2 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PusherSubscriber.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/pushproviders/api/PusherSubscriber.kt @@ -11,14 +11,25 @@ package io.element.android.libraries.pushproviders.api import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.exception.ClientException +/** + * Registers and removes a pusher on the homeserver on behalf of a push provider, so every provider agrees on the parameters sent. + */ interface PusherSubscriber { /** * Register a pusher. Note that failure will be a [RegistrationFailure]. + * + * @param matrixClient the session to register the pusher for. + * @param pushKey the token identifying this device with the gateway. + * @param gateway the URL the homeserver should send the notifications to. */ suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String): Result /** * Unregister a pusher. + * + * @param matrixClient the session to unregister the pusher of. + * @param pushKey the token of the registration to remove. + * @param gateway the gateway of the registration to remove. */ suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String): Result } diff --git a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt index 8a1b2f9f21..cec03e3c78 100644 --- a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt @@ -13,15 +13,36 @@ import kotlinx.coroutines.flow.Flow * Store data related to push about a user. */ interface UserPushStore { + /** The push provider selected for this user, or `null` when none has been selected yet. */ suspend fun getPushProviderName(): String? + + /** + * @param value the name of the push provider to remember for this user. + */ suspend fun setPushProviderName(value: String) + + /** The push key last registered with the homeserver, used to detect that a re-registration is needed; `null` when never registered. */ suspend fun getCurrentRegisteredPushKey(): String? + + /** + * @param value the push key that has just been registered, or `null` to forget it after unregistering. + */ suspend fun setCurrentRegisteredPushKey(value: String?) + /** Whether the user has enabled notifications on this device. */ fun getNotificationEnabledForDevice(): Flow + + /** + * @param enabled true to enable notifications on this device. + */ suspend fun setNotificationEnabledForDevice(enabled: Boolean) + /** Whether the user has asked not to be warned about pusher registration failures. */ fun ignoreRegistrationError(): Flow + + /** + * @param ignore true to stop warning the user about registration failures. + */ suspend fun setIgnoreRegistrationError(ignore: Boolean) /** @@ -29,5 +50,6 @@ interface UserPushStore { */ fun useCompleteNotificationFormat(): Boolean + /** Erases every push preference of this user, to be called when their session is removed. */ suspend fun reset() } diff --git a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt index 78571d8b30..5210602e6c 100644 --- a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt @@ -14,5 +14,10 @@ import io.element.android.libraries.matrix.api.core.SessionId * Store data related to push about a user. */ interface UserPushStoreFactory { + /** + * Returns the push store of a user, creating it on first use. + * + * @param userId the user whose store is requested. + */ fun getOrCreate(userId: SessionId): UserPushStore } diff --git a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecret.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecret.kt index da3ea946f0..a2f4c0ce3b 100644 --- a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecret.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecret.kt @@ -10,15 +10,24 @@ package io.element.android.libraries.pushstore.api.clientsecret import io.element.android.libraries.matrix.api.core.SessionId +/** + * Manages the per-session client secret sent along with a pusher registration. + * + * Push gateways deliver notifications without saying which account they belong to, so the secret is what lets an incoming push be attributed to a session. + */ interface PushClientSecret { /** * To call when registering a pusher. It will return the existing secret or create a new one. + * + * @param userId the user the pusher is being registered for. */ suspend fun getSecretForUser(userId: SessionId): String /** * To call when receiving a push containing a client secret. * Return null if not found. + * + * @param clientSecret the secret carried by the incoming push. */ suspend fun getUserIdFromSecret(clientSecret: String): SessionId? } diff --git a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretFactory.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretFactory.kt index d7ea7d5239..0b425b0115 100644 --- a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretFactory.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretFactory.kt @@ -8,6 +8,10 @@ package io.element.android.libraries.pushstore.api.clientsecret +/** + * Generates the client secrets used to attribute an incoming push to a session; see [PushClientSecret] for how they are used. + */ interface PushClientSecretFactory { + /** Returns a new random secret. */ fun create(): String } diff --git a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretStore.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretStore.kt index ecb87b2b14..c325042ff8 100644 --- a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretStore.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretStore.kt @@ -10,9 +10,37 @@ package io.element.android.libraries.pushstore.api.clientsecret import io.element.android.libraries.matrix.api.core.SessionId +/** + * Persists the mapping between sessions and their push client secrets, in both directions. + */ interface PushClientSecretStore { + /** + * Stores the secret of a user, replacing any previous one. + * + * @param userId the user the secret belongs to. + * @param clientSecret the secret to store. + */ suspend fun storeSecret(userId: SessionId, clientSecret: String) + + /** + * Returns the stored secret of a user, or `null` when they have none yet. + * + * @param userId the user whose secret is requested. + */ suspend fun getSecret(userId: SessionId): String? + + /** + * Forgets the secret of a user, to be called when their session is removed. + * + * @param userId the user whose secret is erased. + */ suspend fun resetSecret(userId: SessionId) + + /** + * Reverse lookup used when a push arrives carrying only a secret. + * + * @param clientSecret the secret to resolve. + * @return the user it belongs to, or `null` when no session matches. + */ suspend fun getUserIdFromSecret(clientSecret: String): SessionId? } diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt index fb60311418..641a2afdf2 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt @@ -11,6 +11,11 @@ package io.element.android.libraries.sessionstorage.api import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +/** + * Persists the logged in sessions and their access tokens, so the app can restore them after a restart. + * + * Sessions are ordered by last usage, which is what makes "the latest session" meaningful across the interface. + */ interface SessionStore { /** * A flow emitting the current logged in state. @@ -27,22 +32,32 @@ interface SessionStore { /** * Add a new session. If other sessions exist, the new one will be set as the latest used one, and * the added session position will be set to a value higher than the other session positions. + * + * @param sessionData the session to store, including its tokens. */ suspend fun addSession(sessionData: SessionData) /** * Will update the session data matching the userId, except the value of loginTimestamp. * No op if userId is not found in DB. + * + * @param sessionData the new content of the session. */ suspend fun updateData(sessionData: SessionData) /** * Update the user profile info of the session matching the userId. + * + * @param sessionId the session to update. + * @param displayName the new display name, or `null` when the user has none. + * @param avatarUrl the new avatar, or `null` when the user has none. */ suspend fun updateUserProfile(sessionId: String, displayName: String?, avatarUrl: String?) /** * Get the session data matching the userId, or null if not found. + * + * @param sessionId the session to read. */ suspend fun getSession(sessionId: String): SessionData? @@ -63,11 +78,15 @@ interface SessionStore { /** * Set the session with [sessionId] as the latest used one. + * + * @param sessionId the session to bring to the front of the ordering. */ suspend fun setLatestSession(sessionId: String) /** * Remove the session matching the sessionId. + * + * @param sessionId the session to forget, which also discards its tokens. */ suspend fun removeSession(sessionId: String) } diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/observer/SessionListener.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/observer/SessionListener.kt index 8dadcafc7d..8d94af5b13 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/observer/SessionListener.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/observer/SessionListener.kt @@ -8,7 +8,22 @@ package io.element.android.libraries.sessionstorage.api.observer +/** + * Reacts to sessions being created or deleted; both methods have an empty default so a listener only overrides what it cares about. + */ interface SessionListener { + /** + * Called after a session has been stored, for instance following a login. + * + * @param userId the session that was created. + */ suspend fun onSessionCreated(userId: String) {} + + /** + * Called after a session has been removed, which is where per-session data should be cleaned up. + * + * @param userId the session that was deleted. + * @param wasLastSession true when no session remains, so app-wide data can be cleaned up too. + */ suspend fun onSessionDeleted(userId: String, wasLastSession: Boolean) {} } diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/observer/SessionObserver.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/observer/SessionObserver.kt index fb120acd56..08925ee688 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/observer/SessionObserver.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/observer/SessionObserver.kt @@ -8,7 +8,17 @@ package io.element.android.libraries.sessionstorage.api.observer +/** + * Notifies interested components when a session is created or deleted, so they can set up or clean up their own per-session data. + */ interface SessionObserver { + /** + * @param listener the listener to notify from now on. + */ fun addListener(listener: SessionListener) + + /** + * @param listener the listener to stop notifying. + */ fun removeListener(listener: SessionListener) } diff --git a/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommandService.kt b/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommandService.kt index 9dfca26078..7ae056e38b 100644 --- a/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommandService.kt +++ b/libraries/slashcommands/api/src/main/kotlin/io/element/android/libraries/slashcommands/api/SlashCommandService.kt @@ -9,7 +9,19 @@ package io.element.android.libraries.slashcommands.api import io.element.android.libraries.matrix.api.timeline.Timeline +/** + * Recognises and runs the slash commands the user can type in the composer, such as `/me` or `/join`. + * + * The composer first [parse]s what was typed, then hands the result to whichever proceed method matches its kind; navigation + * commands and the error cases are handled by the composer itself. + */ interface SlashCommandService { + /** + * Returns the commands whose name starts with what has been typed, so the composer can offer an autocomplete list. + * + * @param text the current composer content. + * @param isInThread whether the composer is in a thread, since not every command is available there. + */ suspend fun getSuggestions( text: String, isInThread: Boolean, @@ -17,6 +29,11 @@ interface SlashCommandService { /** * Parse the message and return a SlashCommand. + * Ordinary text comes back as `NotACommand`, and a malformed or unsupported command as one of the error cases, so this never throws. + * + * @param textMessage the composer content as plain text. + * @param formattedMessage the composer content as HTML, or `null` when unformatted. + * @param isInThreadTimeline whether the composer is in a thread. */ suspend fun parse( textMessage: CharSequence, @@ -26,6 +43,9 @@ interface SlashCommandService { /** * Proceed a SlashCommandSendMessage. + * + * @param slashCommand the parsed command, carrying the content to send. + * @param timeline the timeline to send it to. */ suspend fun proceedSendMessage( slashCommand: SlashCommand.SlashCommandSendMessage, @@ -33,7 +53,9 @@ interface SlashCommandService { ): Result /** - * Proceed a SlashCommandAdmin. + * Proceed a SlashCommandAdmin, i.e. one that acts on the room or its members rather than sending a message. + * + * @param slashCommand the parsed command to run. */ suspend fun proceedAdmin( slashCommand: SlashCommand.SlashCommandAdmin, diff --git a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootNavigator.kt b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootNavigator.kt index a4be22e70b..69a0561d25 100644 --- a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootNavigator.kt +++ b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootNavigator.kt @@ -8,6 +8,10 @@ package io.element.android.libraries.troubleshoot.api.test +/** + * Lets a troubleshooting test's quick fix send the user to another screen, without the test itself knowing about navigation. + */ interface NotificationTroubleshootNavigator { + /** Opens the blocked users screen, so the user can unblock someone whose messages were being filtered out. */ fun navigateToBlockedUsers() } diff --git a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTest.kt b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTest.kt index 27f3bcf8ed..98898ad483 100644 --- a/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTest.kt +++ b/libraries/troubleshoot/api/src/main/kotlin/io/element/android/libraries/troubleshoot/api/test/NotificationTroubleshootTest.kt @@ -20,11 +20,36 @@ import kotlinx.coroutines.flow.StateFlow * the component they're injected into is bound to [SessionScope] and so should these (https://github.com/ZacSweers/metro/issues/1932). */ interface NotificationTroubleshootTest { + /** Position of this test in the list shown to the user; tests run in ascending order. */ val order: Int + + /** The current state of this test, which the UI observes to show it as idle, running, successful or failed. */ val state: StateFlow + + /** + * Whether this test applies to the current setup, so that irrelevant tests are hidden rather than shown as failing. + * + * @param data the current push provider and distributor, used to decide relevance. + */ fun isRelevant(data: TestFilterData): Boolean = true + + /** + * Runs the test, reporting its outcome through [state] rather than by returning or throwing. + * + * @param coroutineScope the scope to launch any work that must outlive the call itself. + */ suspend fun run(coroutineScope: CoroutineScope) + + /** Returns the test to its idle state so it can be run again. */ suspend fun reset() + + /** + * Attempts to fix what this test detected, for the tests that can offer it. Throws by default, so check the state's quick fix + * flag before calling. + * + * @param coroutineScope the scope to launch any work that must outlive the call itself. + * @param navigator used by the fixes that have to send the user to another screen. + */ suspend fun quickFix( coroutineScope: CoroutineScope, navigator: NotificationTroubleshootNavigator, diff --git a/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserListDataSource.kt b/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserListDataSource.kt index 0f7e02c7a8..3697125922 100644 --- a/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserListDataSource.kt +++ b/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserListDataSource.kt @@ -11,8 +11,24 @@ package io.element.android.libraries.usersearch.api import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser +/** + * Low-level access to the homeserver's user directory; see [UserRepository] for the search used by the UI. + */ interface UserListDataSource { - // TODO should probably have a flow + /** + * Searches the user directory, returning an empty list rather than failing when the request does not succeed. + * TODO should probably have a flow + * + * @param query the text to look for in user ids and display names. + * @param count the maximum number of results to return. + */ suspend fun search(query: String, count: Long): List + + /** + * Fetches one user's profile. + * + * @param userId the user whose profile is requested. + * @return the profile, or `null` when it could not be retrieved. + */ suspend fun getProfile(userId: UserId): MatrixUser? } diff --git a/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt b/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt index f7e5367a9a..b3be565ca5 100644 --- a/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt +++ b/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt @@ -10,6 +10,15 @@ package io.element.android.libraries.usersearch.api import kotlinx.coroutines.flow.Flow +/** + * Searches for users to start a conversation with or invite, combining the homeserver directory with what the app already knows. + */ interface UserRepository { + /** + * Runs a search and emits its progress, so the UI can show local results before the directory answers. + * A query that is a well-formed user id other than the current user's also yields that user, even when the directory does not know them. + * + * @param query the text typed by the user. + */ fun search(query: String): Flow } diff --git a/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessagePresenterFactory.kt b/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessagePresenterFactory.kt index 0229ed991f..1b576a6d72 100644 --- a/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessagePresenterFactory.kt +++ b/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessagePresenterFactory.kt @@ -13,7 +13,17 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource import kotlin.time.Duration +/** + * Creates the presenter of a single voice message player, one per voice message shown in a timeline. + */ interface VoiceMessagePresenterFactory { + /** + * @param eventId the event carrying the voice message, or `null` while it is still a local echo. + * @param mediaSource where the audio is fetched from. + * @param mimeType the MIME type of the audio, or `null` when the event did not state it. + * @param filename the file name to display, or `null` when the event did not state it. + * @param duration the length of the recording, used to render the waveform before the audio is downloaded. + */ fun createVoiceMessagePresenter( eventId: EventId?, mediaSource: MediaSource, diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnownParser.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnownParser.kt index db0a323a5f..1466a5b422 100644 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnownParser.kt +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnownParser.kt @@ -7,6 +7,14 @@ package io.element.android.libraries.wellknown.api +/** + * Parses the raw JSON of an Element `.well-known` document. + */ fun interface ElementWellKnownParser { + /** + * Parses the document, failing when it is not valid JSON; unknown fields are ignored so a newer server does not break an older client. + * + * @param json the raw document as served by the server. + */ fun parse(json: String): Result } diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellknownStore.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellknownStore.kt index abb1cf53c7..93256fa6f1 100644 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellknownStore.kt +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellknownStore.kt @@ -7,12 +7,41 @@ package io.element.android.libraries.wellknown.api +/** + * On-disk cache of the Element `.well-known` documents already fetched, so the app has a configuration to work with while offline. + */ interface ElementWellknownStore { + /** + * Reads the cached document of a domain. + * Entries older than a day are reported as outdated rather than missing, so the caller can use them while refreshing; + * a document that no longer parses is reported as an error. + * + * @param domain the server whose cached document is requested. + */ suspend fun get(domain: String): WellknownRetrieverResult + + /** + * Stores the document of a domain and stamps it as fetched now, replacing any previous entry. + * + * @param domain the server the document belongs to. + * @param wellknown the raw document, stored as-is rather than parsed. + */ suspend fun update(domain: String, wellknown: String): Result + + /** + * Removes the cached document of a domain, used when the stored value turns out to be unusable. + * + * @param domain the server whose entry is removed. + */ suspend fun delete(domain: String): Result + /** + * Creates a store, one per configuration source so that their entries never collide. + */ fun interface Factory { + /** + * @param prefix namespaces the cache keys, or `null` for the default well-known endpoint. + */ fun create(prefix: String?): ElementWellknownStore } } diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetriever.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetriever.kt index 56d251426a..9e265bc213 100644 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetriever.kt +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetriever.kt @@ -10,13 +10,30 @@ package io.element.android.libraries.wellknown.api import io.element.android.libraries.matrix.api.UrlContentFetcher +/** + * Retrieves the Element specific `.well-known` configuration of a server, which is how a deployment advertises its own settings. + */ interface WellknownRetriever { + /** + * Returns the configuration of a host, served from the local cache when possible. + * A cached value that has gone stale is returned as outdated while a refresh runs in the background, so this rarely waits on the network. + * An enterprise build with a hardcoded configuration always short-circuits and returns that instead. + * + * @param host the server to look up; a full URL is accepted and reduced to its host. + * @param source which endpoint to read the configuration from, which also selects the cache it is stored in. + */ suspend fun getElementWellKnown( host: String, source: EnterpriseRemoteConfigSource ): WellknownRetrieverResult + /** + * Creates a retriever, which needs a fetcher because the configuration is read over HTTP. + */ interface Factory { + /** + * @param urlContentFetcher used to perform the request; a client that is not authenticated yet also works. + */ fun create(urlContentFetcher: UrlContentFetcher): WellknownRetriever } } diff --git a/libraries/workmanager/api/src/main/kotlin/io/element/android/libraries/workmanager/api/WorkManagerScheduler.kt b/libraries/workmanager/api/src/main/kotlin/io/element/android/libraries/workmanager/api/WorkManagerScheduler.kt index a616e01573..b4c2950cd4 100644 --- a/libraries/workmanager/api/src/main/kotlin/io/element/android/libraries/workmanager/api/WorkManagerScheduler.kt +++ b/libraries/workmanager/api/src/main/kotlin/io/element/android/libraries/workmanager/api/WorkManagerScheduler.kt @@ -10,20 +10,31 @@ package io.element.android.libraries.workmanager.api import io.element.android.libraries.matrix.api.core.SessionId +/** + * Schedules background work, wrapping `WorkManager` so the rest of the app does not depend on it directly. + */ interface WorkManagerScheduler { /** * Submits a new work request built from [workManagerRequestBuilder] to run in `WorkManager`. + * + * @param workManagerRequestBuilder describes the work to schedule. */ suspend fun submit(workManagerRequestBuilder: WorkManagerRequestBuilder) /** * Checks if there are any pending requests scheduled for the provided [sessionId] and [requestType]. + * + * @param sessionId the session the work belongs to. + * @param requestType the kind of work to look for. */ fun hasPendingWork(sessionId: SessionId, requestType: WorkManagerRequestType): Boolean /** * Cancel pending work requests for the session [SessionId]. * If [requestType] is provided, it will only cancel requests for that type, otherwise it will cancel all requests. + * + * @param sessionId the session whose work is cancelled. + * @param requestType the kind of work to cancel, or `null` to cancel every kind. */ fun cancel(sessionId: SessionId, requestType: WorkManagerRequestType? = null) } diff --git a/libraries/workmanager/api/src/main/kotlin/io/element/android/libraries/workmanager/api/di/MetroWorkerFactory.kt b/libraries/workmanager/api/src/main/kotlin/io/element/android/libraries/workmanager/api/di/MetroWorkerFactory.kt index 086d3baafa..d97f66f966 100644 --- a/libraries/workmanager/api/src/main/kotlin/io/element/android/libraries/workmanager/api/di/MetroWorkerFactory.kt +++ b/libraries/workmanager/api/src/main/kotlin/io/element/android/libraries/workmanager/api/di/MetroWorkerFactory.kt @@ -28,7 +28,13 @@ class MetroWorkerFactory( return workerProviders[Class.forName(workerClassName).kotlin]?.create(workerParameters) } + /** + * Creates one worker type with its dependencies injected, since `WorkManager` instantiates workers itself and cannot use the graph. + */ interface WorkerInstanceFactory { + /** + * @param params the parameters `WorkManager` built the worker with. + */ fun create(params: WorkerParameters): T } } diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsSdkManager.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsSdkManager.kt index bf5e04509c..d83690a644 100644 --- a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsSdkManager.kt +++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsSdkManager.kt @@ -13,16 +13,23 @@ package io.element.android.services.analytics.api interface AnalyticsSdkManager { /** * Enable or disable SDK analytics. + * + * @param enabled true to let the SDK report analytics, normally following the user consent. */ fun enableSdkAnalytics(enabled: Boolean) /** * Start a new span with the given [name], using [parentTraceId] to optionally attach it to a parent transaction. + * + * @param name the name of the span. + * @param parentTraceId the trace to attach the span to, or `null` to start a root span. */ fun startSpan(name: String, parentTraceId: String? = null): AnalyticsSdkSpan /** * Create a 'bridge' span optionally linking it to a parent trace via [parentTraceId]. + * + * @param parentTraceId the trace to link the bridge to, or `null` to leave it unattached. */ fun bridge(parentTraceId: String? = null): AnalyticsSdkSpan } diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsService.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsService.kt index 846d5ce5b1..9f29f43b77 100644 --- a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsService.kt +++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsService.kt @@ -15,6 +15,11 @@ import io.element.android.services.analyticsproviders.api.trackers.AnalyticsTrac import io.element.android.services.analyticsproviders.api.trackers.ErrorTracker import kotlinx.coroutines.flow.Flow +/** + * Central analytics entry point: user consent, the analytics identity, and performance transactions. + * + * Nothing is sent to the providers until the user has given their consent through [setUserConsent]. + */ interface AnalyticsService : AnalyticsTracker, ErrorTracker { /** * Get the available analytics providers. @@ -28,6 +33,8 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker { /** * Update the user consent value. + * + * @param userConsent true when the user agrees to analytics being collected. */ suspend fun setUserConsent(userConsent: Boolean) @@ -48,16 +55,26 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker { /** * Update analyticsId from the AccountData. + * + * @param analyticsId the identifier shared across the user's clients, so their events are attributed to one person. */ suspend fun setAnalyticsId(analyticsId: String) /** * Starts a transaction to measure the performance of an operation. + * The caller is responsible for finishing it; see `recordTransaction` for a scoped alternative. + * + * @param name the name the transaction is reported under. + * @param operation the kind of operation being measured. + * @param description a human readable detail shown alongside the measurement. */ fun startTransaction(name: String, operation: String? = null, description: String? = null): AnalyticsTransaction /** * Starts an [AnalyticsLongRunningTransaction], that can be shared with other components. + * + * @param longRunningTransaction which of the known long running operations is starting. + * @param parentTransaction the transaction to attach this one to, or `null` to start a root transaction. */ fun startLongRunningTransaction( longRunningTransaction: AnalyticsLongRunningTransaction, @@ -66,15 +83,25 @@ interface AnalyticsService : AnalyticsTracker, ErrorTracker { /** * Gets an ongoing [AnalyticsLongRunningTransaction], if it exists. + * + * @param longRunningTransaction the long running operation to look up. */ fun getLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction): AnalyticsTransaction? /** * Removes an ongoing [AnalyticsLongRunningTransaction] so it's no longer shared. + * + * @param longRunningTransaction the long running operation to stop sharing. + * @return the transaction that was removed, or `null` when none was ongoing. */ fun removeLongRunningTransaction(longRunningTransaction: AnalyticsLongRunningTransaction): AnalyticsTransaction? - /** Enter a span inside the Rust SDK tracing system. If a [parentTraceId] is provided, the SDK trace will be added as a child of that trace. */ + /** + * Enter a span inside the Rust SDK tracing system. If a [parentTraceId] is provided, the SDK trace will be added as a child of that trace. + * + * @param name the name of the span. + * @param parentTraceId the trace to attach the span to, or `null` to start a root span. + */ @Discouraged("This method can cause crashes of the app when using debug builds of the Rust SDK.") fun enterSdkSpan(name: String?, parentTraceId: String?): AnalyticsSdkSpan } diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/ScreenTracker.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/ScreenTracker.kt index 29513f66ff..2a93b2f7aa 100644 --- a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/ScreenTracker.kt +++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/ScreenTracker.kt @@ -11,7 +11,15 @@ package io.element.android.services.analytics.api import androidx.compose.runtime.Composable import im.vector.app.features.analytics.plan.MobileScreen +/** + * Reports screen views to analytics from Composable code. + */ interface ScreenTracker { + /** + * Records a view of [screen] when this enters composition, so it is called once per appearance rather than on every recomposition. + * + * @param screen the screen being shown. + */ @Composable fun TrackScreen( screen: MobileScreen.ScreenName, diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/watchers/AnalyticsColdStartWatcher.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/watchers/AnalyticsColdStartWatcher.kt index 67d3c5498b..905b0dc88f 100644 --- a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/watchers/AnalyticsColdStartWatcher.kt +++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/watchers/AnalyticsColdStartWatcher.kt @@ -12,7 +12,12 @@ package io.element.android.services.analytics.api.watchers * until the cached room list is displayed. This check only takes place in a cold app start after the user is authenticated. */ interface AnalyticsColdStartWatcher { + /** Begins the measurement, to be called as early as possible in the cold start. */ fun start() + + /** Restarts the measurement from the login flow, since a user who has to sign in first is not a normal cold start. */ fun whenLoggingIn() + + /** Ends the measurement, to be called once the cached room list is actually on screen. */ fun onRoomListVisible() } diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/watchers/AnalyticsRoomListStateWatcher.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/watchers/AnalyticsRoomListStateWatcher.kt index 2f6d4723a3..85cb210d8c 100644 --- a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/watchers/AnalyticsRoomListStateWatcher.kt +++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/watchers/AnalyticsRoomListStateWatcher.kt @@ -12,6 +12,9 @@ package io.element.android.services.analytics.api.watchers * the app was previously running and we just returned to it. */ interface AnalyticsRoomListStateWatcher { + /** Begins watching the room list state, to be called when the app returns to the foreground. */ fun start() + + /** Stops watching, to be called when the app leaves the foreground. */ fun stop() } diff --git a/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/AnalyticsProvider.kt b/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/AnalyticsProvider.kt index 593d62f40f..7b6278740c 100644 --- a/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/AnalyticsProvider.kt +++ b/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/AnalyticsProvider.kt @@ -11,15 +11,29 @@ package io.element.android.services.analyticsproviders.api import io.element.android.services.analyticsproviders.api.trackers.AnalyticsTracker import io.element.android.services.analyticsproviders.api.trackers.ErrorTracker +/** + * One analytics backend, such as PostHog or Sentry. + * + * Providers are driven by the analytics service rather than used directly, and only receive data once the user has consented. + */ interface AnalyticsProvider : AnalyticsTracker, ErrorTracker { /** * User friendly name. */ val name: String + /** Starts the backend, to be called when the user consents to analytics. */ fun init() + /** Stops the backend and drops any pending data, to be called when the user withdraws their consent. */ fun stop() + /** + * Starts a performance transaction, returning `null` when this provider does not support them. + * + * @param name the name the transaction is reported under. + * @param operation the kind of operation being measured. + * @param description a human readable detail shown alongside the measurement. + */ fun startTransaction(name: String, operation: String? = null, description: String? = null): AnalyticsTransaction? } diff --git a/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/AnalyticsTransaction.kt b/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/AnalyticsTransaction.kt index 82d337914a..f54cadbda8 100644 --- a/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/AnalyticsTransaction.kt +++ b/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/AnalyticsTransaction.kt @@ -9,6 +9,11 @@ package io.element.android.services.analyticsproviders.api import kotlin.time.Duration +/** + * A performance measurement in progress, which reports its timing once [finish] is called. + * + * Transactions can nest through [startChild]; see `recordChildTransaction` for a scoped alternative that always finishes the child. + */ interface AnalyticsTransaction { /** * The time elapsed since the transaction started until now if the transaction is ongoing or the time it finished. @@ -17,11 +22,17 @@ interface AnalyticsTransaction { /** * Start a child span from this transaction. + * + * @param operation the kind of operation the child measures. + * @param description a human readable detail shown alongside the measurement. */ fun startChild(operation: String, description: String? = null): AnalyticsTransaction /** * Adds extra data to the transaction. This data is not indexed, it's just listed. + * + * @param key the name of the field. + * @param value the value to attach. */ fun putExtraData(key: String, value: String) @@ -29,6 +40,9 @@ interface AnalyticsTransaction { * Similar to [putExtraData], adds extra data that *will be indexed* and can be used for filtering in the analytics portal. * * **Do not add numerical values using this function, use [putExtraData] instead.** + * + * @param key the name of the indexed field. + * @param value the value to attach. */ fun putIndexableData(key: String, value: String) @@ -44,6 +58,8 @@ interface AnalyticsTransaction { /** * Attach a throwable to the transaction, so we can know it failed. + * + * @param throwable the failure that occurred during the measured operation. */ fun attachError(throwable: Throwable) diff --git a/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/trackers/AnalyticsTracker.kt b/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/trackers/AnalyticsTracker.kt index 5bb63338a8..db4a8a5517 100644 --- a/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/trackers/AnalyticsTracker.kt +++ b/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/trackers/AnalyticsTracker.kt @@ -14,19 +14,28 @@ import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.SuperProperties import im.vector.app.features.analytics.plan.UserProperties +/** + * Records analytics events; implemented by every provider and by the analytics service that fans out to them. + */ interface AnalyticsTracker { /** * Capture an Event. + * + * @param event the event to record, taken from the shared analytics plan. */ fun capture(event: VectorAnalyticsEvent) /** * Track a displayed screen. + * + * @param screen the screen that was shown. */ fun screen(screen: VectorAnalyticsScreen) /** * Update user specific properties. + * + * @param userProperties the properties to merge into the current user's profile. */ fun updateUserProperties(userProperties: UserProperties) diff --git a/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/trackers/ErrorTracker.kt b/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/trackers/ErrorTracker.kt index 5277493b64..94a04b842b 100644 --- a/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/trackers/ErrorTracker.kt +++ b/services/analyticsproviders/api/src/main/kotlin/io/element/android/services/analyticsproviders/api/trackers/ErrorTracker.kt @@ -8,6 +8,14 @@ package io.element.android.services.analyticsproviders.api.trackers +/** + * Reports non-fatal errors to the crash reporting backend. + */ interface ErrorTracker { + /** + * Records an error that was handled but is still worth knowing about. + * + * @param throwable the failure to report; never pass one whose message can contain user content. + */ fun trackError(throwable: Throwable) } diff --git a/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateService.kt b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateService.kt index 7c592e2102..d9ae37c097 100644 --- a/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateService.kt +++ b/services/apperror/api/src/main/kotlin/io/element/android/services/apperror/api/AppErrorStateService.kt @@ -11,10 +11,26 @@ package io.element.android.services.apperror.api import androidx.annotation.StringRes import kotlinx.coroutines.flow.StateFlow +/** + * Surfaces app-wide error dialogs from anywhere, for the failures that are not tied to the screen the user is on. + */ interface AppErrorStateService { + /** The error to display, if any; observed once at the top of the app rather than per screen. */ val appErrorStateFlow: StateFlow + /** + * Shows an error dialog with already resolved text. + * + * @param title the dialog title. + * @param body the dialog message; never put user content or secrets in it. + */ fun showError(title: String, body: String) + /** + * Shows an error dialog from string resources, which is the preferred form since it stays localised. + * + * @param titleRes resource id of the dialog title. + * @param bodyRes resource id of the dialog message. + */ fun showError(@StringRes titleRes: Int, @StringRes bodyRes: Int) } diff --git a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppForegroundStateService.kt b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppForegroundStateService.kt index 33c2ee133b..8bfa747583 100644 --- a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppForegroundStateService.kt +++ b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppForegroundStateService.kt @@ -34,27 +34,41 @@ interface AppForegroundStateService { */ val isSyncingNotificationEvent: StateFlow + /** + * Updates to whether the app is publishing a live location will be emitted here. + */ val isSharingLiveLocation: StateFlow /** - * Start observing the foreground state. + * Start observing the foreground state; to be called once when the app process starts. */ fun startObservingForeground() /** * Update the in-call state. + * + * @param isInCall true while the user is in a call, which keeps the session syncing even in the background. */ fun updateIsInCallState(isInCall: Boolean) /** * Update the 'has ringing call' state. + * + * @param hasRingingCall true while an incoming call is ringing and has not yet been answered or declined. */ fun updateHasRingingCall(hasRingingCall: Boolean) /** * Update the active state for the syncing notification event flow. + * + * @param isSyncingNotificationEvent true while a push is being resolved, so the sync is not stopped mid-way. */ fun updateIsSyncingNotificationEvent(isSyncingNotificationEvent: Boolean) + /** + * Update the live location sharing state. + * + * @param isSharingLiveLocation true while at least one live location share is active on this device. + */ fun updateIsSharingLiveLocation(isSharingLiveLocation: Boolean) } diff --git a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateService.kt b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateService.kt index cf32bb0bf4..d6373f44c3 100644 --- a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateService.kt +++ b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateService.kt @@ -17,14 +17,55 @@ import kotlinx.coroutines.flow.StateFlow * A service that tracks the navigation and foreground states of the app. */ interface AppNavigationStateService { + /** + * Where the user currently is, as a stack that deepens from root to session, room and thread. + * Used by the components that need to know what is on screen, notably to suppress the notifications of the room being read. + */ val appNavigationState: StateFlow + /** + * Pushes a session onto the navigation state. + * + * @param owner identifies the navigation node reporting this, so that a later leave call can be matched to it. + * @param sessionId the session that was opened. + */ fun onNavigateToSession(owner: String, sessionId: SessionId) + + /** + * Pops back to the root. Does nothing when [owner] is not the owner of the current state, which is how out-of-order + * calls from a node that has already been replaced are ignored. + * + * @param owner the same value passed to [onNavigateToSession]. + */ fun onLeavingSession(owner: String) + /** + * Pushes a room onto the navigation state. Logs an error and does nothing unless a session was pushed first. + * + * @param owner identifies the navigation node reporting this. + * @param roomId the room that was opened. + */ fun onNavigateToRoom(owner: String, roomId: RoomId) + + /** + * Pops back to the parent session; as with [onLeavingSession], a mismatched [owner] makes this a no-op. + * + * @param owner the same value passed to [onNavigateToRoom]. + */ fun onLeavingRoom(owner: String) + /** + * Pushes a thread onto the navigation state. Logs an error and does nothing unless a room was pushed first. + * + * @param owner identifies the navigation node reporting this. + * @param threadId the thread that was opened. + */ fun onNavigateToThread(owner: String, threadId: ThreadId) + + /** + * Pops back to the parent room; as with [onLeavingSession], a mismatched [owner] makes this a no-op. + * + * @param owner the same value passed to [onNavigateToThread]. + */ fun onLeavingThread(owner: String) } diff --git a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/intent/ExternalIntentLauncher.kt b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/intent/ExternalIntentLauncher.kt index ba34a6a716..acd58c9f64 100644 --- a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/intent/ExternalIntentLauncher.kt +++ b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/intent/ExternalIntentLauncher.kt @@ -14,5 +14,10 @@ import android.content.Intent * Used to launch external intents from anywhere in the app. */ interface ExternalIntentLauncher { + /** + * Starts the given intent, doing nothing when no installed app can handle it. + * + * @param intent the intent to hand over to the system. + */ fun launch(intent: Intent) } diff --git a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/sdk/BuildVersionSdkIntProvider.kt b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/sdk/BuildVersionSdkIntProvider.kt index 26bbe23548..385109d6c8 100644 --- a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/sdk/BuildVersionSdkIntProvider.kt +++ b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/sdk/BuildVersionSdkIntProvider.kt @@ -10,6 +10,9 @@ package io.element.android.services.toolbox.api.sdk import androidx.annotation.ChecksSdkIntAtLeast +/** + * Exposes the running Android API level, injected rather than read from `Build.VERSION` so tests can pretend to be on any version. + */ interface BuildVersionSdkIntProvider { /** * Return the current version of the Android SDK. @@ -17,7 +20,11 @@ interface BuildVersionSdkIntProvider { fun get(): Int /** - * Checks the if the current OS version is equal or greater than [version]. + * Checks if the current OS version is equal or greater than [version], and run [result] if so. + * + * @param T the type of the result to compute. + * @param version the minimum API level required. + * @param result computed and returned only when the requirement is met. * @return A `non-null` result if true, `null` otherwise. */ @ChecksSdkIntAtLeast(parameter = 0, lambda = 1) @@ -29,6 +36,11 @@ interface BuildVersionSdkIntProvider { } } + /** + * Whether the current OS version is equal or greater than [version]. + * + * @param version the minimum API level to test against. + */ @ChecksSdkIntAtLeast(parameter = 0) fun isAtLeast(version: Int) = get() >= version } diff --git a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/strings/StringProvider.kt b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/strings/StringProvider.kt index 4570e09921..f82d542f7e 100644 --- a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/strings/StringProvider.kt +++ b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/strings/StringProvider.kt @@ -11,6 +11,9 @@ package io.element.android.services.toolbox.api.strings import androidx.annotation.PluralsRes import androidx.annotation.StringRes +/** + * Resolves Android string resources outside of Composable and Context-aware code, so presenters stay testable. + */ interface StringProvider { /** * Returns a localized string from the application's package's @@ -39,6 +42,10 @@ interface StringProvider { * Returns a localized formatted string from the application's package's * default string table, substituting the format arguments as defined in * [java.util.Formatter] and [java.lang.String.format], based on the given quantity. + * + * @param resId Resource id for the plurals. + * @param quantity The quantity used to select the plural form. + * @param formatArgs The format arguments that will be used for substitution. */ fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any?): String diff --git a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/systemclock/SystemClock.kt b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/systemclock/SystemClock.kt index 5436b44354..52a189f607 100644 --- a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/systemclock/SystemClock.kt +++ b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/systemclock/SystemClock.kt @@ -8,6 +8,10 @@ package io.element.android.services.toolbox.api.systemclock +/** + * Reads the current time, injected rather than called statically so that tests can control it. + */ fun interface SystemClock { + /** The current wall-clock time in milliseconds since the epoch. */ fun epochMillis(): Long }