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:
| Approach | Best For | Method |
|---|
| Approach A: Header-Based Requests | OkHttp, custom networking | SecureMediaHelper.createSecureOkHttpClientBuilder() |
| Approach B: Presigned URLs | Third-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.
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.
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}")
}
})
AppSettings appSettings = new AppSettings.AppSettingsBuilder()
.setSecureMediaMode(SecureMediaMode.HEADER_BASED) // Enable header-based mode
.setRegion("us")
.build();
CometChat.init(this, appID, appSettings, new CometChat.CallbackListener<String>() {
@Override
public void onSuccess(String successMessage) {
Log.d(TAG, "CometChat initialized with secure media");
}
@Override
public void onError(CometChatException e) {
Log.e(TAG, "Initialization failed: " + e.getMessage());
}
});
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)
// Create OkHttpClient with FAT header injection
OkHttpClient client = SecureMediaHelper.createSecureOkHttpClientBuilder().build();
// Load an image
public void loadImage(String urlString, ImageView imageView) {
Request request = new Request.Builder()
.url(urlString)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "Failed to load image: " + e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.body() != null) {
InputStream inputStream = response.body().byteStream();
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
runOnUiThread(() -> imageView.setImageBitmap(bitmap));
}
}
});
}
// Usage
loadImage(mediaMessage.getAttachment().getFileUrl(), myImageView);
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
}
public void playVideo(String url, Context context) {
Map<String, String> headers = SecureMediaHelper.getSecureHeaders(url);
MediaPlayer mediaPlayer = new MediaPlayer();
try {
mediaPlayer.setDataSource(context, Uri.parse(url), headers);
mediaPlayer.prepareAsync();
mediaPlayer.setOnPreparedListener(mp -> mp.start());
} catch (IOException e) {
Log.e(TAG, "Error playing video: " + e.getMessage());
}
}
// For ExoPlayer
public ExoPlayer playVideoWithExoPlayer(String url, Context context) {
Map<String, String> headers = SecureMediaHelper.getSecureHeaders(url);
DefaultHttpDataSource.Factory dataSourceFactory = new DefaultHttpDataSource.Factory()
.setDefaultRequestProperties(headers);
MediaItem mediaItem = MediaItem.fromUri(url);
ExoPlayer player = new ExoPlayer.Builder(context).build();
player.setMediaSource(
new 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}")
}
})
AppSettings appSettings = new AppSettings.AppSettingsBuilder()
.setRegion("us")
.build();
CometChat.init(this, appID, appSettings, new CometChat.CallbackListener<String>() {
@Override
public void onSuccess(String successMessage) {
Log.d(TAG, "CometChat initialized");
}
@Override
public void onError(CometChatException e) {
Log.e(TAG, "Initialization failed: " + e.getMessage());
}
});
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)
// With Glide
public void loadImageWithGlide(String urlString, ImageView imageView) {
CometChat.fetchPresignedUrl(
urlString,
new CometChat.CallbackListener<String>() {
@Override
public void onSuccess(String presignedUrl) {
// Use the presigned URL with Glide
Glide.with(imageView.getContext())
.load(presignedUrl)
.into(imageView);
}
@Override
public void onError(CometChatException e) {
Log.e(TAG, "Failed to get presigned URL: " + e.getMessage());
}
}
);
}
// With Picasso
public void loadImageWithPicasso(String urlString, ImageView imageView) {
CometChat.fetchPresignedUrl(
urlString,
new CometChat.CallbackListener<String>() {
@Override
public void onSuccess(String presignedUrl) {
// Use the presigned URL with Picasso
Picasso.get()
.load(presignedUrl)
.into(imageView);
}
@Override
public void onError(CometChatException e) {
Log.e(TAG, "Failed to get presigned URL: " + e.getMessage());
}
}
);
}
// Usage
loadImageWithGlide(mediaMessage.getAttachment().getFileUrl(), 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}")
}
}
)
}
public void loadImageWithRetry(String url, ImageView imageView, int retryCount) {
CometChat.fetchPresignedUrl(
url,
new CometChat.CallbackListener<String>() {
@Override
public void onSuccess(String presignedUrl) {
Glide.with(imageView.getContext())
.load(presignedUrl)
.listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(
GlideException e,
Object model,
Target<Drawable> target,
boolean isFirstResource
) {
// Check for expiry (403 Forbidden)
if (retryCount < 1) {
loadImageWithRetry(url, imageView, retryCount + 1);
}
return false;
}
@Override
public boolean onResourceReady(
Drawable resource,
Object model,
Target<Drawable> target,
DataSource dataSource,
boolean isFirstResource
) {
return false;
}
})
.into(imageView);
}
@Override
public void onError(CometChatException e) {
Log.e(TAG, "Error: " + e.getMessage());
}
}
);
}
Methods and Enums
enum class SecureMediaMode {
EMBEDDED, // FAT in URL as ?fat=xxx (default)
HEADER_BASED // FAT sent via HTTP header (secure)
}
public enum SecureMediaMode {
EMBEDDED, // FAT in URL as ?fat=xxx (default)
HEADER_BASED // FAT sent via HTTP header (secure)
}
| Mode | Value | Description |
|---|
EMBEDDED | 0 | Token appended to URL as ?fat=xxx (default behavior) |
HEADER_BASED | 1 | Token sent via HTTP fat header (more secure) |
Creates an OkHttpClient.Builder with the FAT token interceptor configured.
fun createSecureOkHttpClientBuilder(): OkHttpClient.Builder
public static OkHttpClient.Builder createSecureOkHttpClientBuilder()
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>
)
public static void fetchPresignedUrl(
String url,
CometChat.CallbackListener<String> callback
)
| Parameter | Type | Description |
|---|
| url | String | CometChat media URL (attachment, avatar, group icon) |
| callback | CallbackListener<String> | Callback with presigned URL string or error |
Error Codes:
| Code | Description |
|---|
ERR_NOT_LOGGED_IN | User must be logged in |
ERR_INVALID_URL | Invalid CometChat media URL |
ERR_NETWORK | Network request failed |
ERR_INVALID_RESPONSE | Invalid server response |
Returns the current FAT token for manual header injection.
public static String getFat()
Returns: FAT token string, or null if user is not logged in.
| Method | Description | Returns |
|---|
isHeaderModeEnabled() | Checks if header mode is enabled and configured | Boolean |
requiresSecureAccess(url: String) | Checks if a URL requires FAT header injection | Boolean |
hasFatQueryParam(url: String) | Checks if URL contains a FAT query parameter | Boolean |
stripFatQueryParam(url: String) | Removes FAT query parameter from URL | String |
getSecureMediaHost() | Returns the secure media host from backend | String? |
getSecureHeaders(url: String) | Returns headers map for MediaPlayer/ExoPlayer | Map<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)
}
})
}
public void downloadFile(String urlString, Consumer<File> onComplete) {
OkHttpClient client = SecureMediaHelper.createSecureOkHttpClientBuilder().build();
Request request = new Request.Builder()
.url(urlString)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
onComplete.accept(null);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.body() != null) {
String fileName = Uri.parse(urlString).getLastPathSegment();
if (fileName == null) fileName = "download";
File file = new File(context.getCacheDir(), fileName);
try (InputStream input = response.body().byteStream();
OutputStream output = new FileOutputStream(file)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
}
onComplete.accept(file);
} else {
onComplete.accept(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() }
}
}
public MediaPlayer playAudio(String url, Context context) throws IOException {
Map<String, String> headers = SecureMediaHelper.getSecureHeaders(url);
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(context, Uri.parse(url), headers);
mediaPlayer.prepareAsync();
mediaPlayer.setOnPreparedListener(MediaPlayer::start);
return mediaPlayer;
}
Troubleshooting
401 Unauthorized
| Cause | Solution |
|---|
| User not logged in | Ensure login completes before loading media |
| FAT not included | Use SecureMediaHelper.createSecureOkHttpClientBuilder() or fetchPresignedUrl() |
| Wrong mode | Verify 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()}")
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)
// ❌ Wrong - no FAT header
MediaPlayer player = new MediaPlayer();
player.setDataSource(videoUrl);
// ✅ Correct - FAT header injected
Map<String, String> headers = SecureMediaHelper.getSecureHeaders(videoUrl);
MediaPlayer player = new 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) {}
})
// ❌ Wrong - caching presigned URL
String cachedUrl = presignedUrl; // Don't do this!
// ✅ Correct - fetch fresh each time
CometChat.fetchPresignedUrl(originalUrl, new CometChat.CallbackListener<String>() {
@Override
public void onSuccess(String freshUrl) {
// Use immediately
}
@Override
public void onError(CometChatException e) {}
});
Migration Guide
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()
// Before (embedded mode - default)
AppSettings appSettings = new AppSettings.AppSettingsBuilder()
.setRegion("us")
.build();
// After (header-based mode)
AppSettings appSettings = new AppSettings.AppSettingsBuilder()
.setSecureMediaMode(SecureMediaMode.HEADER_BASED) // Add this line
.setRegion("us")
.build();
// Before
val client = OkHttpClient()
val request = Request.Builder().url(url).build()
// After
val client = SecureMediaHelper.createSecureOkHttpClientBuilder().build()
val request = Request.Builder().url(url).build()
// Before
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(url).build();
// After
OkHttpClient client = SecureMediaHelper.createSecureOkHttpClientBuilder().build();
Request request = new Request.Builder().url(url).build();
// Before
val player = MediaPlayer()
player.setDataSource(videoUrl)
// After
val headers = SecureMediaHelper.getSecureHeaders(videoUrl)
val player = MediaPlayer()
player.setDataSource(context, Uri.parse(videoUrl), headers)
// Before
MediaPlayer player = new MediaPlayer();
player.setDataSource(videoUrl);
// After
Map<String, String> headers = SecureMediaHelper.getSecureHeaders(videoUrl);
MediaPlayer player = new MediaPlayer();
player.setDataSource(context, Uri.parse(videoUrl), headers);