The Media Enhancement API provides a low-latency, privacy-preserving, on-device AI solution that leverages hardware acceleration to deliver high-quality media improvements with zero APK bloat. For more information, see Understand the media enhancement capabilities.
The static Bitmap mode (EnhancementMode.BITMAP) is designed for static,
decoded image processing.
Operating in Bitmap mode requires the OS to serialize uncompressed pixel data and copy it from CPU memory across the system bus to GPU memory, returning the processed frame through a reverse copy. It is optimized for single-frame execution and isn't compatible with real-time video streaming.
This workflow involves creating a session, processing a single bitmap with it, and then handling the result. The code examples from the previous sections cover this use case in detail.
- Configure options: Create an
EnhancementOptionsobject, ensuring theenhancementModeis set toEnhancementMode.BITMAP. - Create session: Use the
createSessionAsyncwrapper we defined earlier to create anEnhancementSession. This is a heavyweight object, so create it only when needed. - Process image: Call the session, your input bitmap, and the options you want.
- Handle result: The suspend function returns a new, enhanced Bitmap on success or throw an exception on failure.
- Release session: Crucially, call
session.release()when you are finished to free up GPU resources.
The EnhancementSession is a heavyweight context object that maintains a
persistent GPU or NPU memory pipeline. It allocates dedicated video RAM (VRAM)
and native system handles. To prevent severe memory leaks and potential
OutOfMemoryError crashes, adhere to the following lifecycle principles:
- Lazy instantiation: Don't create a session until the user initiates an enhancement action.
- Strategic reuse: Maintain and reuse a single session instance when processing multiple images with identical configurations (dimensions and toggled options).
- Prompt teardown: Invoke
session.release()immediately when visual tasks terminate to free shared hardware resources.
Initialize the enhancement engine
This method orchestrates a two-step check. It verifies if the device's hardware supports acceleration, and then ensures the required machine learning modules are present.
Running this as a prerequisite step prevents runtime initialization failures by validating capabilities before your app attempts to process media.
class MediaSetupViewModel(application: Application) : AndroidViewModel(application) {
private val enhancementClient = Enhancement.getClient(application)
fun initializeEnhancementEngine() {
viewModelScope.launch {
try {
// 1. Verify hardware capability
val isSupported = enhancementClient.isDeviceSupportedAsync()
if (!isSupported) {
notifyUiDeviceIncompatible()
return@launch
}
// 2. Verify and download the Google Play services ML modules
val isInstalled = enhancementClient.isModuleInstalledAsync()
if (!isInstalled) {
notifyUiDownloadingModels()
enhancementClient.installModule().await()
}
notifyUiEngineReady()
} catch (e: Exception) {
// Handle potential errors during session creation or image processing.
handleInitializationError(e)
}
}
}
}
Create session and bitmap process wrappers
Use these Kotlin coroutine wrappers to convert task-based client callbacks into standard suspending functions, enabling cleaner and sequential execution.
// Wraps the task-based createSession callback into a suspending function.
suspend fun EnhancementClient.createSessionAsync(
options: EnhancementOptions,
executor: Executor
): EnhancementSession = withContext(Dispatchers.Main) {
suspendCancellableCoroutine { continuation ->
// EnhancementSessionCallback handles session success or failure.
val callback = object : EnhancementSessionCallback {
override fun onSessionCreated(session: EnhancementSession) {
continuation.resume(session)
}
override fun onSessionCreationFailed(status: Status) {
continuation.resumeWithException(
Exception("Session creation failed: ${status.statusMessage} (${status.statusCode})")
)
}
override fun onSessionDestroyed() {}
override fun onSessionDisconnected(status: Status) {}
}
// Handles errors during the initial request trigger.
this@createSessionAsync.createSession(options, callback).addOnFailureListener(executor) { e ->
if (continuation.isActive) {
continuation.resumeWithException(e)
}
}
}
}
// Wraps this process in a suspending function for cleaner execution.
suspend fun EnhancementSession.processBitmapAsync(
bitmap: Bitmap,
options: EnhancementOptions
): Bitmap = suspendCancellableCoroutine { continuation ->
// EnhancementCallback returns the processed bitmap or an error code.
val callback = object : EnhancementCallback {
override fun onBitmapProcessed(enhancedBitmap: Bitmap) {
continuation.resume(enhancedBitmap)
}
override fun onError(statusCode: Int) {
continuation.resumeWithException(
Exception("Bitmap processing failed with status code: $statusCode")
)
}
override fun onSurfaceProcessed(timestamp: Long) {}
}
this.process(bitmap, options, callback)
}
Execute the bitmap pipeline in a ViewModel
To integrate the enhancement pipeline into your application architecture, use a
ViewModel to manage the session lifecycle. This approach ensures that the
heavy GPU resources are released when the ViewModel is cleared.
// Define a data class to hold image information.
data class ImageInfo(val bitmap: Bitmap)
// Define a UI state class to hold loading status, errors, and enhanced image.
data class EnhancementUiState(
val isLoading: Boolean = false,
val enhancementError: String? = null,
val enhancedImage: ImageInfo? = null
)
class EnhancementViewModel(application: Application) : AndroidViewModel(application) {
// Backing field for UI state, initialized with default values.
private val _uiState = MutableStateFlow(EnhancementUiState())
// Publicly exposed UI state flow for observation.
val uiState: StateFlow<EnhancementUiState> = _uiState.asStateFlow()
// Initialize client to interact with the Media Enhancement service.
private val enhancementClient: EnhancementClient = Enhancement.getClient(application)
// Single-thread executor for processing background enhancement tasks.
private val enhancementExecutor = Executors.newSingleThreadExecutor()
// Track session state to enable reuse across multiple processing calls.
private var enhancementSession: EnhancementSession? = null
private var sessionOptions: EnhancementOptions? = null
// Primary function to trigger the enhancement workflow for a provided bitmap.
fun enhanceImage(bitmap: Bitmap) {
viewModelScope.launch(Dispatchers.IO) {
_uiState.update { it.copy(isLoading = true, enhancementError = null) }
try {
// Define enhancement options (for example, enable upscale, tonemapping) based
// on bitmap dimensions.
val options = EnhancementOptions(
bitmap.width,
bitmap.height,
EnhancementMode.BITMAP,
isTonemappingEnabled = true,
isDeblurAndDenoisePhotoEnabled = true,
isUpscalePhotoEnabled = false,
)
// 1. Establish the session lazily on demand. A session is configured for
// specific input dimensions, so replace it when the bitmap size changes.
if (enhancementSession == null ||
sessionOptions?.width != options.width ||
sessionOptions?.height != options.height
) {
enhancementSession?.release()
enhancementSession = null
enhancementSession = enhancementClient.createSessionAsync(options, enhancementExecutor)
sessionOptions = options
}
val session = enhancementSession ?: throw IllegalStateException("Session unavailable.")
// 2. Dispatch image through the neural pipeline
val enhancedBitmap = session.processBitmapAsync(bitmap, options)
// 3. Render output to UI
_uiState.update {
it.copy(enhancedImage = ImageInfo(bitmap = enhancedBitmap))
}
} catch (e: Exception) {
_uiState.update { it.copy(enhancementError = e.message) }
} finally {
// Ensure loading state is reset regardless of the outcome.
_uiState.update { it.copy(isLoading = false) }
}
}
}
override fun onCleared() {
// 4. Critical: Release native GPU hardware resources
enhancementSession?.release()
enhancementSession = null
enhancementExecutor.shutdown()
super.onCleared()
}
}
Load images progressively on slow networks
On slow networks, your app can download a smaller, more compressed version of an image instead of the full-resolution file. Your app can then use Bitmap mode to upscale and deblur the image on the device. The enhanced image replaces the full-resolution download, so users see a sharper image without a large file transfer.
When to use this pattern
This pattern is a good fit when all of the following are true:
- The device is supported:
isDeviceSupportedAsync()returnstrue. The API requires Android 11 (API level 30) or higher and is optimized for premium-tier devices. - The image is displayed larger than its downloaded size: For example, a small image shown full screen. If the image stays at thumbnail size, upscaling adds little visible benefit.
- The enhancement module is already installed:
isModuleInstalledAsync()returnstrue. Module installation downloads model files over the network. If the user is already on a slow connection, don't start the installation.
If any of these conditions isn't met, display the downloaded image without enhancement.
Implement the pattern
The pattern works as follows:
- Display the low-resolution image: Render the small image as soon as it downloads.
- Enhance in the background: If the enhancement engine is ready, process the image in Bitmap mode with upscaling and deblurring enabled.
- Swap in the result: Replace the displayed image with the enhanced bitmap. A short cross-fade makes the swap less noticeable.
The following ProgressiveImageEnhancer class processes one image at a time and
reuses a single session for images of the same size. If enhancement fails,
enhance() returns null so that your app continues to display the
low-resolution image. The class uses the createSessionAsync and
processBitmapAsync wrappers defined in Create session and bitmap process
wrappers.
Install the module ahead of time, as shown in Initialize the enhancement
engine. If isDeviceSupportedAsync() and isModuleInstalledAsync() return
true, call enhance().
class ProgressiveImageEnhancer(
private val enhancementClient: EnhancementClient,
private val executor: Executor,
) {
// Callers can request enhancements concurrently, but they all share one
// session, so process one image at a time.
private val mutex = Mutex()
private var session: EnhancementSession? = null
private var sessionOptions: EnhancementOptions? = null
// Enhancement is optional for this pattern, so return null instead of
// throwing. The caller can keep the low-resolution image without extra
// error handling.
suspend fun enhance(lowResBitmap: Bitmap): Bitmap? = mutex.withLock {
val options = EnhancementOptions(
width = lowResBitmap.width,
height = lowResBitmap.height,
enhancementMode = EnhancementMode.BITMAP,
isDeblurAndDenoisePhotoEnabled = true,
isUpscalePhotoEnabled = true,
)
try {
getOrCreateSession(options).processBitmapAsync(lowResBitmap, options)
} catch (e: CancellationException) {
// A cancelled request (for example, the user scrolled away) isn't
// an enhancement failure, so let the caller's scope handle it.
throw e
} catch (e: Exception) {
null
}
}
// Sessions are expensive to create and are configured for specific input
// dimensions, so reuse the current one until the image size changes.
private suspend fun getOrCreateSession(options: EnhancementOptions): EnhancementSession {
val currentSession = session
val currentOptions = sessionOptions
if (currentSession != null &&
currentOptions != null &&
currentOptions.width == options.width &&
currentOptions.height == options.height
) {
return currentSession
}
releaseSession()
return enhancementClient.createSessionAsync(options, executor).also {
session = it
sessionOptions = options
}
}
// A session holds GPU memory until it's released, so free it as soon as
// the images it enhances are no longer on screen. Taking the lock first
// waits for any in-progress enhancement, so a session that's still being
// created isn't leaked.
suspend fun release() = mutex.withLock { releaseSession() }
private fun releaseSession() {
session?.release()
session = null
sessionOptions = null
}
}
When you use this pattern, follow these guidelines:
- Request a consistent size: An
EnhancementSessionuses fixed input dimensions. Request low-resolution images at one consistent size so that your app can reuse a single session across images. - Cache the enhanced result: Store the enhanced bitmap in your image cache so that your app doesn't process the same image again. If you enable upscaling, the enhanced bitmap is larger than the input bitmap. Size your cache for the larger bitmaps.