Hardware Bitmap Limitation in the Cobrowse Android SDK
The Cobrowse Android SDK cannot capture screen content when hardware bitmaps are present in the view hierarchy. For example, ImageView components or Compose Image composables). This is a known limitation of the Android screen capture API when hardware-accelerated bitmaps are used.
When hardware bitmaps are present during an active Cobrowse session, the SDK will render affected screens as black images to avoid exposing sensitive or redacted data.
You will see the following error printed to logcat:
Cannot capture the app view: Software rendering doesn't support hardware bitmaps.
One common scenario is the Coil image-loading library, which uses hardware bitmaps by default.
Suggested fixes
- Use software rendering during an active Cobrowse Session
- Use a conditional
ImageLoader - Disable hardware rendering globally
Please see the details of each suggestion below.
Use software rendering during an active Cobrowse Session
To ensure proper screen capture during an active Cobrowse sessions you can configure Coil to use software bitmaps then revert to hardware bitmaps once the Cobrowse session ends.
/**
* A delegate that provides callbacks for handling Cobrowse session lifecycle events.
*/
class CobrowseDelegate : CobrowseIO.Delegate {
override fun sessionDidUpdate(session: Session) {
CobrowseSessionState.rememberSessionState(this, session.isActive)
}
override fun sessionDidEnd(session: Session) {
CobrowseSessionState.rememberSessionState(this, false)
}
}
/**
* Use a mutable state to track whether we are in a Cobrowse session.
* Recomposition will occur if composables read `isInCobrowseSession.value`.
*/
object CobrowseSessionState {
/**
* A session state flag to enable or disable hardware bitmaps in the app.
*/
var isInCobrowseSession = mutableStateOf(false)
/**
* Make sure to call this method from the Cobrowse delegate methods.
*/
fun rememberSessionState(
context: Context,
hasActiveSession: Boolean) {
// Ignore if the state is not changing
if (hasActiveSession && isInCobrowseSession.value) return
if (!hasActiveSession && !isInCobrowseSession.value) return
isInCobrowseSession.value = hasActiveSession
}
}
// Usage of Coil's `AsyncImage`
val imageUrl = "https://www.gstatic.com/webp/gallery/1.jpg"
val inSession = CobrowseSessionState.isInCobrowseSession.value
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.memoryCacheKey("AsyncImage url=${imageUrl} session=${inSession}")
.allowHardware(inSession)
.build(),
contentDescription = null,
)
Use a conditional ImageLoader
If you want to keep AsyncImage usage unchanged you can set up a conditional ImageLoader.
/**
* A delegate that provides callbacks for handling Cobrowse session lifecycle events.
*/
class CobrowseDelegate : CobrowseIO.Delegate {
override fun sessionDidUpdate(session: Session) {
CobrowseSessionState.rememberSessionState(this, session.isActive)
}
override fun sessionDidEnd(session: Session) {
CobrowseSessionState.rememberSessionState(this, false)
}
}
/**
* Use a mutable state to track whether we are in a Cobrowse session.
* Recomposition will occur if composables read `isInCobrowseSession.value`.
*/
object CobrowseSessionState {
/**
* A session state flag to enable or disable hardware bitmaps in the app.
*/
var isInCobrowseSession = mutableStateOf(false)
/**
* Make sure to call this method from the CobrowseIO delegate methods.
*/
fun rememberSessionState(
context: Context,
hasActiveSession: Boolean) {
// Ignore if the state is not changing
if (hasActiveSession && isInCobrowseSession.value) return
if (!hasActiveSession && !isInCobrowseSession.value) return
isInCobrowseSession.value = hasActiveSession
// Configure whether hardware bitmaps are allowed during screen capture.
// When set to true, enables the use of hardware-accelerated bitmaps,
// when set to false, forces software rendering.
Coil.setImageLoader(
ImageLoader.Builder(context = context)
.allowHardware(!hasActiveSession)
.build()
)
}
}
// Wrap any Composables that might depend on the session state
// in a `key(isInCobrowseSession.value) { ... }` block
// to force recomposition when `isInCobrowseSession` changes.
val imageUrl = "https://www.gstatic.com/webp/gallery/1.jpg"
val inSession = CobrowseSessionState.isInCobrowseSession.value
key(inSession) {
AsyncImage(
model = imageUrl,
contentDescription = null
)
}
Disable hardware rendering globally
Alternatively, you can disable hardware bitmaps in Coil globally.
class MainApplication : Application(), ImageLoaderFactory {
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this)
.allowHardware(false)
.build()
}
}