public interface FileUploader {
@WorkerThread
public fun sendFile(
channelType: String,
channelId: String,
userId: String,
file: File,
callback: ProgressCallback,
): Result<UploadedFile>
@WorkerThread
public fun sendImage(
channelType: String,
channelId: String,
userId: String,
file: File,
callback: ProgressCallback,
): Result<UploadedFile>
@WorkerThread
public fun deleteFile(
channelType: String,
channelId: String,
userId: String,
url: String,
): Result<Unit>
@WorkerThread
public fun deleteImage(
channelType: String,
channelId: String,
userId: String,
url: String,
): Result<Unit>
}Custom CDN
By default, files and images are uploaded to Stream's CDN and loaded without additional HTTP headers. However, you can customize both behaviors:
- File Uploads: Provide a custom
FileUploaderto upload files to your own CDN or storage service. - CDN Reads: Provide a custom
CDNimplementation to intercept and transform every file/image loading request (signed URLs, auth headers, custom hosts).
Custom File Uploads
To use a custom CDN or storage service (such as AWS S3, Firebase Storage, or your own backend), implement the FileUploader interface and configure it on the ChatClient.Builder.
FileUploader Interface
The FileUploader interface defines the contract for uploading and deleting files and images:
The interface also includes overloaded versions of these methods without the ProgressCallback parameter, as well as methods for uploading/deleting files not associated with a specific channel (uploadFile, uploadImage, deleteFile, deleteImage). You will need to implement those as well (omitted in this snippet for brevity).
UploadedFile Model
When a file is successfully uploaded, return an UploadedFile containing the file URL and optional metadata:
public data class UploadedFile(
val file: String, // The URL of the uploaded file
val thumbUrl: String? = null, // Optional thumbnail URL (for videos)
val extraData: Map<String, Any> = emptyMap(), // Additional custom metadata
)Creating a Custom FileUploader
Here's an example of a custom FileUploader implementation:
class CustomFileUploader(
private val uploadApi: YourUploadApi,
) : FileUploader {
override fun sendFile(
channelType: String,
channelId: String,
userId: String,
file: File,
callback: ProgressCallback,
): Result<UploadedFile> {
return try {
val response = uploadApi.uploadFile(
file = file,
path = "channels/$channelType/$channelId/files",
onProgress = { bytesUploaded, totalBytes ->
callback.onProgress(bytesUploaded, totalBytes)
}
)
Result.Success(
UploadedFile(
file = response.url,
thumbUrl = response.thumbnailUrl,
)
)
} catch (e: Exception) {
callback.onError(e)
Result.Failure(Error.ThrowableError(e.message ?: "Upload failed", e))
}
}
override fun sendImage(
channelType: String,
channelId: String,
userId: String,
file: File,
callback: ProgressCallback,
): Result<UploadedFile> {
return try {
val response = uploadApi.uploadImage(
file = file,
path = "channels/$channelType/$channelId/images",
onProgress = { bytesUploaded, totalBytes ->
callback.onProgress(bytesUploaded, totalBytes)
}
)
Result.Success(
UploadedFile(
file = response.url,
thumbUrl = response.thumbnailUrl,
)
)
} catch (e: Exception) {
callback.onError(e)
Result.Failure(Error.ThrowableError(e.message ?: "Upload failed", e))
}
}
override fun deleteFile(
channelType: String,
channelId: String,
userId: String,
url: String,
): Result<Unit> {
return try {
uploadApi.deleteFile(url)
Result.Success(Unit)
} catch (e: Exception) {
Result.Failure(Error.ThrowableError(e.message ?: "Delete failed", e))
}
}
override fun deleteImage(
channelType: String,
channelId: String,
userId: String,
url: String,
): Result<Unit> {
return try {
uploadApi.deleteFile(url)
Result.Success(Unit)
} catch (e: Exception) {
Result.Failure(Error.ThrowableError(e.message ?: "Delete failed", e))
}
}
// Implement the remaining methods (without ProgressCallback, and channel-agnostic variants) similarly
}Configuring the Custom FileUploader
Pass your custom FileUploader to the ChatClient.Builder:
val client = ChatClient.Builder(apiKey, context)
.fileUploader(CustomFileUploader(YourUploadApi()))
.build()File Transformation Before Upload
The SDK supports transforming files before upload using the FileTransformer interface. This is useful for any preprocessing steps, such as image compression or format conversion.
public fun interface FileTransformer {
@WorkerThread
public fun transform(file: File): File
}Example implementation:
class ImageCompressorTransformer : FileTransformer {
override fun transform(file: File): File {
val mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(file.extension)
// Only compress image files
if (mimeType?.startsWith("image/") != true) {
return file
}
return compressImage(file)
}
private fun compressImage(file: File): File {
// Your compression logic here
}
}File transformation happens before the file is passed to the FileUploader.
Custom CDN Reads
If your CDN requires signed URLs, authentication tokens, or custom headers when loading files and images, implement the CDN interface and configure it on the ChatClient.Builder. This provides a single, unified configuration point that covers all media loading paths in the SDK:
- Images loaded via Coil (avatars, image attachments, link previews)
- Videos and audio played via ExoPlayer (video attachments, voice recordings)
- File downloads via OkHttp and Android's
DownloadManager - Document previews opened via external apps
CDN Interface
The CDN interface defines two suspend methods, each returning a CDNRequest:
public interface CDN {
public suspend fun imageRequest(url: String): CDNRequest = CDNRequest(url)
public suspend fun fileRequest(url: String): CDNRequest = CDNRequest(url)
}imageRequest— called for image URLs (Coil loading, image attachment download).fileRequest— called for non-image files (video/audio playback, document opening, file download).
Both methods have default implementations that return the original URL unchanged, so you only need to override the methods relevant to your CDN.
The CDN methods are suspend but are not guaranteed to be called on a background dispatcher. If your implementation performs network calls (e.g., fetching a signed URL from a remote service), wrap them in withContext(Dispatchers.IO).
CDNRequest
CDNRequest holds the (potentially rewritten) URL and optional headers:
public data class CDNRequest(
val url: String,
val headers: Map<String, String>? = null,
)Configuring the CDN
Register the CDN on ChatClient via the builder:
ChatClient.Builder("apiKey", context)
.cdn(myCdnImpl)
.build()Example: Signed URLs with Authentication Headers
val myCdn = object : CDN {
override suspend fun imageRequest(url: String): CDNRequest = withContext(Dispatchers.IO) {
val signedUrl = mySigningService.sign(url)
CDNRequest(
url = signedUrl,
headers = mapOf("Authorization" to "Bearer ${getToken()}")
)
}
override suspend fun fileRequest(url: String): CDNRequest = withContext(Dispatchers.IO) {
val signedUrl = mySigningService.sign(url)
CDNRequest(
url = signedUrl,
headers = mapOf("Authorization" to "Bearer ${getToken()}")
)
}
}
ChatClient.Builder("apiKey", context)
.cdn(myCdn)
.build()Supported Image MIME Types
The SDK recognizes the following MIME types as images:
image/bmpimage/gifimage/jpegimage/pngimage/webpimage/heicimage/heic-sequenceimage/heifimage/heif-sequenceimage/svg+xml
Files with these MIME types are uploaded using sendImage(), while all other files use sendFile().