Skip to main content

Overview

Secure Media Access provides enhanced security for file transfers in CometChat by transmitting the File Access Token (FAT) via HTTP headers instead of URL query parameters. This prevents token exposure in server logs, browser history, and network monitoring tools. By default, CometChat uses the EMBEDDED mode, which appends the FAT token to media URLs as a query parameter (?fat=xxx). While functional, this approach can expose tokens in logs and browser history. The HEADER_BASED mode offers a more secure alternative by transmitting the token via HTTP headers instead.
If you’re using CometChat UIKit, no action is required. The UIKit handles secure media automatically.

Requirements

  • Android SDK v4.1.0+
  • Backend support for secure media access

Choose Your Approach

There are two ways to load media securely. Choose the one that fits your use case:
ApproachBest ForMethod
Approach A: Header-Based RequestsOkHttp, custom networkingSecureMediaHelper.createSecureOkHttpClientBuilder()
Approach B: Presigned URLsThird-party libraries (Glide, Coil, Picasso)CometChat.fetchPresignedUrl()
Pick ONE approach and use it consistently throughout your app. Do not mix both approaches for the same media type.

Approach A: Header-Based Requests

Use this approach when you have direct control over the network request (e.g., using OkHttp). The SDK injects the FAT token into the HTTP headers automatically.

Step 1: Enable Header-Based Mode

Add .setSecureMediaMode(SecureMediaMode.HEADER_BASED) during SDK initialization:
val appSettings = AppSettings.AppSettingsBuilder()
    .setSecureMediaMode(SecureMediaMode.HEADER_BASED)  // Enable header-based mode
    .setRegion("us")
    .build()

CometChat.init(this, appID, appSettings, object : CometChat.CallbackListener<String>() {
    override fun onSuccess(successMessage: String?) {
        Log.d(TAG, "CometChat initialized with secure media")
    }

    override fun onError(e: CometChatException?) {
        Log.e(TAG, "Initialization failed: ${e?.message}")
    }
})

Step 2: Load Media Using Header-Based Requests

Use SecureMediaHelper.createSecureOkHttpClientBuilder() to create an OkHttpClient with the FAT header interceptor:
// Create OkHttpClient with FAT header injection
val client = SecureMediaHelper.createSecureOkHttpClientBuilder().build()

// Load an image
fun loadImage(urlString: String, imageView: ImageView) {
    val request = Request.Builder()
        .url(urlString)
        .build()
    
    client.newCall(request).enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            Log.e(TAG, "Failed to load image: ${e.message}")
        }

        override fun onResponse(call: Call, response: Response) {
            response.body?.byteStream()?.let { inputStream ->
                val bitmap = BitmapFactory.decodeStream(inputStream)
                runOnUiThread {
                    imageView.setImageBitmap(bitmap)
                }
            }
        }
    })
}

// Usage
loadImage(mediaMessage.attachment?.fileUrl ?: "", myImageView)

Step 3: Handle Video/Audio with MediaPlayer

For MediaPlayer, use SecureMediaHelper.getSecureHeaders() to get the headers map:
fun playVideo(url: String, context: Context) {
    val headers = SecureMediaHelper.getSecureHeaders(url)
    
    val mediaPlayer = MediaPlayer()
    mediaPlayer.setDataSource(context, Uri.parse(url), headers)
    mediaPlayer.prepareAsync()
    mediaPlayer.setOnPreparedListener { mp ->
        mp.start()
    }
}

// For ExoPlayer
fun playVideoWithExoPlayer(url: String, context: Context): ExoPlayer {
    val headers = SecureMediaHelper.getSecureHeaders(url)
    
    val dataSourceFactory = DefaultHttpDataSource.Factory()
        .setDefaultRequestProperties(headers)
    
    val mediaItem = MediaItem.fromUri(url)
    val player = ExoPlayer.Builder(context).build()
    
    player.setMediaSource(
        ProgressiveMediaSource.Factory(dataSourceFactory)
            .createMediaSource(mediaItem)
    )
    player.prepare()
    
    return player
}

Approach B: Presigned URLs

Use this approach when working with third-party image loading libraries like Glide, Coil, or Picasso. These libraries don’t support custom headers easily, so you fetch a temporary presigned URL instead.
Presigned URLs expire after approximately 5 minutes. Always fetch a fresh URL before each load. Never cache presigned URLs.

Step 1: Initialize SDK (No Special Mode Required)

Presigned URLs work regardless of the secure media mode setting:
val appSettings = AppSettings.AppSettingsBuilder()
    .setRegion("us")
    .build()

CometChat.init(this, appID, appSettings, object : CometChat.CallbackListener<String>() {
    override fun onSuccess(successMessage: String?) {
        Log.d(TAG, "CometChat initialized")
    }

    override fun onError(e: CometChatException?) {
        Log.e(TAG, "Initialization failed: ${e?.message}")
    }
})

Step 2: Fetch Presigned URL and Load Media

Use CometChat.fetchPresignedUrl() to get a temporary URL, then pass it to your image library:
// With Glide
fun loadImageWithGlide(urlString: String, imageView: ImageView) {
    CometChat.fetchPresignedUrl(
        urlString,
        object : CometChat.CallbackListener<String>() {
            override fun onSuccess(presignedUrl: String) {
                // Use the presigned URL with Glide
                Glide.with(imageView.context)
                    .load(presignedUrl)
                    .into(imageView)
            }

            override fun onError(e: CometChatException) {
                Log.e(TAG, "Failed to get presigned URL: ${e.message}")
            }
        }
    )
}

// With Coil
fun loadImageWithCoil(urlString: String, imageView: ImageView) {
    CometChat.fetchPresignedUrl(
        urlString,
        object : CometChat.CallbackListener<String>() {
            override fun onSuccess(presignedUrl: String) {
                // Use the presigned URL with Coil
                imageView.load(presignedUrl)
            }

            override fun onError(e: CometChatException) {
                Log.e(TAG, "Failed to get presigned URL: ${e.message}")
            }
        }
    )
}

// Usage
loadImageWithGlide(mediaMessage.attachment?.fileUrl ?: "", myImageView)

Step 3: Handle URL Expiry with Retry Logic

Since presigned URLs expire, implement retry logic for 403 errors:
fun loadImageWithRetry(url: String, imageView: ImageView, retryCount: Int = 0) {
    CometChat.fetchPresignedUrl(
        url,
        object : CometChat.CallbackListener<String>() {
            override fun onSuccess(presignedUrl: String) {
                Glide.with(imageView.context)
                    .load(presignedUrl)
                    .listener(object : RequestListener<Drawable> {
                        override fun onLoadFailed(
                            e: GlideException?,
                            model: Any?,
                            target: Target<Drawable>,
                            isFirstResource: Boolean
                        ): Boolean {
                            // Check for expiry (403 Forbidden)
                            if (retryCount < 1) {
                                loadImageWithRetry(url, imageView, retryCount + 1)
                            }
                            return false
                        }

                        override fun onResourceReady(
                            resource: Drawable,
                            model: Any,
                            target: Target<Drawable>?,
                            dataSource: DataSource,
                            isFirstResource: Boolean
                        ): Boolean {
                            return false
                        }
                    })
                    .into(imageView)
            }

            override fun onError(e: CometChatException) {
                Log.e(TAG, "Error: ${e.message}")
            }
        }
    )
}

Methods and Enums

SecureMediaMode Enum

enum class SecureMediaMode {
    EMBEDDED,      // FAT in URL as ?fat=xxx (default)
    HEADER_BASED   // FAT sent via HTTP header (secure)
}
ModeValueDescription
EMBEDDED0Token appended to URL as ?fat=xxx (default behavior)
HEADER_BASED1Token sent via HTTP fat header (more secure)

SecureMediaHelper.createSecureOkHttpClientBuilder()

Creates an OkHttpClient.Builder with the FAT token interceptor configured.
fun createSecureOkHttpClientBuilder(): OkHttpClient.Builder
Returns: OkHttpClient.Builder with FAT interceptor if in header-based mode. Behavior:
  • Strips existing ?fat= query parameter from URL
  • Adds FAT header if URL requires secure access
  • Decodes percent-encoded FAT token before adding

CometChat.fetchPresignedUrl()

Fetches a temporary presigned URL for CometChat media. Works regardless of SecureMediaMode.
fun fetchPresignedUrl(
    url: String,
    callback: CometChat.CallbackListener<String>
)
ParameterTypeDescription
urlStringCometChat media URL (attachment, avatar, group icon)
callbackCallbackListener<String>Callback with presigned URL string or error
Error Codes:
CodeDescription
ERR_NOT_LOGGED_INUser must be logged in
ERR_INVALID_URLInvalid CometChat media URL
ERR_NETWORKNetwork request failed
ERR_INVALID_RESPONSEInvalid server response

SecureMediaHelper.getFat()

Returns the current FAT token for manual header injection.
fun getFat(): String?
Returns: FAT token string, or null if user is not logged in.

SecureMediaHelper Utility Methods

MethodDescriptionReturns
isHeaderModeEnabled()Checks if header mode is enabled and configuredBoolean
requiresSecureAccess(url: String)Checks if a URL requires FAT header injectionBoolean
hasFatQueryParam(url: String)Checks if URL contains a FAT query parameterBoolean
stripFatQueryParam(url: String)Removes FAT query parameter from URLString
getSecureMediaHost()Returns the secure media host from backendString?
getSecureHeaders(url: String)Returns headers map for MediaPlayer/ExoPlayerMap<String, String>

Additional Examples

Download File (Approach A)

fun downloadFile(urlString: String, onComplete: (File?) -> Unit) {
    val client = SecureMediaHelper.createSecureOkHttpClientBuilder().build()
    
    val request = Request.Builder()
        .url(urlString)
        .build()
    
    client.newCall(request).enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            onComplete(null)
        }

        override fun onResponse(call: Call, response: Response) {
            response.body?.let { body ->
                val fileName = Uri.parse(urlString).lastPathSegment ?: "download"
                val file = File(context.cacheDir, fileName)
                
                file.outputStream().use { output ->
                    body.byteStream().copyTo(output)
                }
                
                onComplete(file)
            } ?: onComplete(null)
        }
    })
}

Play Audio (Approach A)

fun playAudio(url: String, context: Context): MediaPlayer {
    val headers = SecureMediaHelper.getSecureHeaders(url)
    
    return MediaPlayer().apply {
        setDataSource(context, Uri.parse(url), headers)
        prepareAsync()
        setOnPreparedListener { start() }
    }
}

Troubleshooting

401 Unauthorized

CauseSolution
User not logged inEnsure login completes before loading media
FAT not includedUse SecureMediaHelper.createSecureOkHttpClientBuilder() or fetchPresignedUrl()
Wrong modeVerify setSecureMediaMode(SecureMediaMode.HEADER_BASED) was called (Approach A only)

Images Not Loading

Debug with:
Log.d(TAG, "Header mode enabled: ${SecureMediaHelper.isHeaderModeEnabled()}")
Log.d(TAG, "FAT available: ${SecureMediaHelper.getFat() != null}")
Log.d(TAG, "Secure host: ${SecureMediaHelper.getSecureMediaHost()}")

Video Won’t Play

// ❌ Wrong - no FAT header
val player = MediaPlayer()
player.setDataSource(videoUrl)

// ✅ Correct - FAT header injected
val headers = SecureMediaHelper.getSecureHeaders(videoUrl)
val player = MediaPlayer()
player.setDataSource(context, Uri.parse(videoUrl), headers)

Presigned URL Expired (403)

Always fetch a fresh presigned URL before each load:
// ❌ Wrong - caching presigned URL
val cachedUrl = presignedUrl  // Don't do this!

// ✅ Correct - fetch fresh each time
CometChat.fetchPresignedUrl(originalUrl, object : CometChat.CallbackListener<String>() {
    override fun onSuccess(freshUrl: String) {
        // Use immediately
    }
    override fun onError(e: CometChatException) {}
})

Migration Guide

Upgrading from Embedded to Header-Based Mode

If you’re currently using the default EMBEDDED mode and want to switch to HEADER_BASED:

1. Update Initialization

// Before (embedded mode - default)
val appSettings = AppSettings.AppSettingsBuilder()
    .setRegion("us")
    .build()

// After (header-based mode)
val appSettings = AppSettings.AppSettingsBuilder()
    .setSecureMediaMode(SecureMediaMode.HEADER_BASED)  // Add this line
    .setRegion("us")
    .build()

2. Update Media Loading Code

// Before
val client = OkHttpClient()
val request = Request.Builder().url(url).build()

// After
val client = SecureMediaHelper.createSecureOkHttpClientBuilder().build()
val request = Request.Builder().url(url).build()

3. Update MediaPlayer Code

// Before
val player = MediaPlayer()
player.setDataSource(videoUrl)

// After
val headers = SecureMediaHelper.getSecureHeaders(videoUrl)
val player = MediaPlayer()
player.setDataSource(context, Uri.parse(videoUrl), headers)