Add on-device AI image enhancement feature to enhance images#194
Add on-device AI image enhancement feature to enhance images#194MayuriKhinvasara wants to merge 7 commits into
Conversation
…hat screen Major changes: - Integrate the Google Play Services Media Effect Enhancement library to support on-device AI processing. - Implement `ImageEnhancementScreen` and `EnhancementViewModel` to handle image enhancement features, including tonemapping, deblurring, and upscaling. - Add `EnhancementUtils.kt` providing coroutine-based wrappers for enhancement session management, bitmap processing, and module installation. Minor changes: - Add an "AI Enhance" action button to image bubbles within the chat UI. - Update the `ChatMessage` model to include a unique ID and add support for updating media URIs in the `ChatRepository` and `MessageDao`. - Define navigation logic for the new enhancement pane in `Main.kt` and `SocialiteNavigation.kt`. - Update the README to include documentation for the Media Effect Enhancement integration.
There was a problem hiding this comment.
Code Review
This pull request adds on-device AI image enhancement features (tonemapping, deblurring, and upscaling) using Google Play Services, introducing a new enhancement screen, ViewModel, and database updates to persist enhanced image URIs. The review feedback identifies several critical issues: a data loss bug where saving an enhanced image deletes all other enhanced images in the chat history, thread safety issues with concurrent access to enhancementSession, and incorrect coroutine continuation handling during module installation. Additionally, the reviewer recommends removing silent background module downloads to conserve data, avoiding passing short-lived Context objects to the ViewModel to prevent memory leaks, and catching Throwable to safely handle OutOfMemoryError during bitmap decoding.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| override fun onInstalled() { | ||
| Log.d("EnhancementUtils", "onInstalled") | ||
| } |
There was a problem hiding this comment.
Resume the coroutine continuation with true when the module is successfully installed. Currently, the continuation is not resumed here, which means if the installation takes time, the caller is never notified of the actual completion via this callback.
| override fun onInstalled() { | |
| Log.d("EnhancementUtils", "onInstalled") | |
| } | |
| override fun onInstalled() { | |
| Log.d("EnhancementUtils", "onInstalled") | |
| if (continuation.isActive) continuation.resume(true) | |
| } |
There was a problem hiding this comment.
Done. Added continuation.resume(true) to ensure the caller is correctly notified when the module finishes installing.
| .addOnSuccessListener { result -> | ||
| if (continuation.isActive) continuation.resume(result) | ||
| } |
There was a problem hiding this comment.
Only resume the continuation immediately if the task result is true (indicating the module is already installed). If the result is false, the installation has just been initiated, and we must wait for the onInstalled() callback to resume the continuation. Resuming immediately on task success causes the ViewModel to prematurely mark the module as ready while it is still downloading in the background.
.addOnSuccessListener { result ->
if (result && continuation.isActive) {
continuation.resume(true)
}
}There was a problem hiding this comment.
Done. Updated the listener to only resume immediately if the result is true. Otherwise, we properly wait for the onInstalled() callback.
| // First check if it's already installed | ||
| val installed = client.isModuleInstalledAsync() | ||
| if (!installed) { | ||
| // If not installed, attempt to install it before querying device support | ||
| try { | ||
| client.installModuleAsync { progress -> | ||
| // Silent background install progress | ||
| } | ||
| } catch (e: Exception) { | ||
| Log.e("EnhancementSupport", "Failed to silently install module", e) | ||
| } | ||
| } |
There was a problem hiding this comment.
Remove the silent background installation from checkSupport. This function is called on the main chat screen to quickly determine if the "AI Enhance" button should be shown. Silently downloading and installing a large AI module in the background without user consent can consume significant cellular data and battery. Additionally, isDeviceSupportedAsync() can be queried directly without downloading the module first. The module installation is already handled with a proper progress UI when the user explicitly navigates to the ImageEnhancementScreen.
There was a problem hiding this comment.
Done. Removed the silent background installation. The module is now only installed when the user explicitly navigates to the enhancement screen.
| fun saveEnhancedImage(messageId: Long, onComplete: () -> Unit) { | ||
| val enhancedBitmap = _uiState.value.enhancedImage?.bitmap ?: return | ||
| viewModelScope.launch(Dispatchers.IO) { | ||
| try { | ||
| val context = getApplication<Application>() | ||
| val directory = File(context.filesDir, "media") | ||
| if (!directory.exists()) { | ||
| directory.mkdirs() | ||
| } | ||
|
|
||
| // Prevent storage bloat by deleting previous enhanced files | ||
| directory.listFiles()?.forEach { file -> | ||
| if (file.name.startsWith("enhanced_")) { | ||
| file.delete() | ||
| } | ||
| } |
There was a problem hiding this comment.
Instead of deleting all files starting with "enhanced_", pass the oldUri to saveEnhancedImage and only delete that specific file if it was indeed a previously enhanced file for this message. Currently, this deletes enhanced images for all other messages in the chat history, causing broken image links and data loss.
| fun saveEnhancedImage(messageId: Long, onComplete: () -> Unit) { | |
| val enhancedBitmap = _uiState.value.enhancedImage?.bitmap ?: return | |
| viewModelScope.launch(Dispatchers.IO) { | |
| try { | |
| val context = getApplication<Application>() | |
| val directory = File(context.filesDir, "media") | |
| if (!directory.exists()) { | |
| directory.mkdirs() | |
| } | |
| // Prevent storage bloat by deleting previous enhanced files | |
| directory.listFiles()?.forEach { file -> | |
| if (file.name.startsWith("enhanced_")) { | |
| file.delete() | |
| } | |
| } | |
| fun saveEnhancedImage(messageId: Long, oldUri: String, onComplete: () -> Unit) { | |
| val enhancedBitmap = _uiState.value.enhancedImage?.bitmap ?: return | |
| viewModelScope.launch(Dispatchers.IO) { | |
| try { | |
| val context = getApplication<Application>() | |
| val directory = File(context.filesDir, "media") | |
| if (!directory.exists()) { | |
| directory.mkdirs() | |
| } | |
| // Prevent storage bloat by deleting only the previous enhanced file for this message | |
| val oldFileUri = Uri.parse(oldUri) | |
| if (oldFileUri.scheme == "file") { | |
| val oldFile = File(oldFileUri.path ?: "") | |
| if (oldFile.exists() && oldFile.name.startsWith("enhanced_")) { | |
| oldFile.delete() | |
| } | |
| } |
There was a problem hiding this comment.
Done. Now passing oldUri to selectively delete only the previously enhanced file for this specific message, preventing accidental deletion of others.
| } | ||
| Button( | ||
| onClick = { | ||
| enhancementViewModel.saveEnhancedImage(messageId, onFinishEditing) |
There was a problem hiding this comment.
Pass the current uri to saveEnhancedImage so that only the previous enhanced file for this specific message is deleted, preventing accidental deletion of enhanced images from other messages.
| enhancementViewModel.saveEnhancedImage(messageId, onFinishEditing) | |
| enhancementViewModel.saveEnhancedImage(messageId, uri, onFinishEditing) |
There was a problem hiding this comment.
Done. Passed the current uri into saveEnhancedImage to support the targeted deletion.
| enhancementSession?.release() | ||
| enhancementSession = null |
There was a problem hiding this comment.
Confine the release of enhancementSession to the Main thread to ensure thread safety.
| enhancementSession?.release() | |
| enhancementSession = null | |
| // Release any previous session, since we have a new image. Confined to Main thread. | |
| withContext(Dispatchers.Main) { | |
| enhancementSession?.release() | |
| enhancementSession = null | |
| } |
There was a problem hiding this comment.
Done. Wrapped the session release in withContext(Dispatchers.Main) to synchronize access. This guarantees that our coroutines and the Android lifecycle's onCleared() method take turns accessing the enhancementSession variable one at a time, completely eliminating thread-safety issues and leaks without freezing the UI.
| super.onCleared() | ||
| } | ||
|
|
||
| fun onImageSelected(uri: Uri, context: Context) { |
There was a problem hiding this comment.
Remove the context parameter from onImageSelected. Passing short-lived contexts (like Activity context) to ViewModels is an architectural anti-pattern that can cause memory leaks. Since EnhancementViewModel is an AndroidViewModel, it can safely access the application context via getApplication().
| fun onImageSelected(uri: Uri, context: Context) { | |
| fun onImageSelected(uri: Uri) { |
There was a problem hiding this comment.
Done. Removed the context parameter and switched to using getApplication() to prevent memory leaks.
| enhancementSession?.release() | ||
| enhancementSession = null | ||
|
|
||
| val originalBitmap = decodeBitmapFromUri(uri, context) |
There was a problem hiding this comment.
Done. Updated to use getApplication() when decoding the bitmap.
|
|
||
| LaunchedEffect(uri) { | ||
| enhancementViewModel.setEnhancementMode(EnhancementMode.BITMAP) | ||
| enhancementViewModel.onImageSelected(Uri.parse(uri), context) |
There was a problem hiding this comment.
Done. Dropped the context argument here since the ViewModel handles it internally now.
| context.contentResolver.openInputStream(uri)?.use { inputStream -> | ||
| BitmapFactory.decodeStream(inputStream) | ||
| } | ||
| } catch (e: Exception) { |
There was a problem hiding this comment.
Catch Throwable instead of Exception to safely handle OutOfMemoryError during bitmap decoding. OutOfMemoryError is an Error (not an Exception), so it will bypass the current catch (e: Exception) block and crash the app if the decoded image is too large.
| } catch (e: Exception) { | |
| } catch (e: Throwable) { |
There was a problem hiding this comment.
Done. Changed the catch block to Throwable to safely catch and handle OutOfMemoryError crashes.
…hat screen Major changes: - Integrate the Google Play Services Media Effect Enhancement library to support on-device AI processing. - Implement `ImageEnhancementScreen` and `EnhancementViewModel` to handle image enhancement features, including tonemapping, deblurring, and upscaling. - Add `EnhancementUtils.kt` providing coroutine-based wrappers for enhancement session management, bitmap processing, and module installation. Minor changes: - Add an "AI Enhance" action button to image bubbles within the chat UI. - Update the `ChatMessage` model to include a unique ID and add support for updating media URIs in the `ChatRepository` and `MessageDao`. - Define navigation logic for the new enhancement pane in `Main.kt` and `SocialiteNavigation.kt`. - Update the README to include documentation for the Media Effect Enhancement integration.
…ncement # Conflicts: # app/build.gradle.kts # app/src/main/java/com/google/android/samples/socialite/repository/ChatRepository.kt # app/src/main/java/com/google/android/samples/socialite/ui/Main.kt # app/src/main/java/com/google/android/samples/socialite/ui/chat/component/MessageBubble.kt # app/src/main/java/com/google/android/samples/socialite/ui/mediaenhancement/EnhancementUtils.kt # app/src/main/java/com/google/android/samples/socialite/ui/mediaenhancement/EnhancementViewModel.kt # app/src/main/java/com/google/android/samples/socialite/ui/mediaenhancement/ImageEnhancementScreen.kt
…ncement # Conflicts: # app/build.gradle.kts # app/src/main/java/com/google/android/samples/socialite/repository/ChatRepository.kt # app/src/main/java/com/google/android/samples/socialite/ui/Main.kt # app/src/main/java/com/google/android/samples/socialite/ui/chat/component/MessageBubble.kt # app/src/main/java/com/google/android/samples/socialite/ui/mediaenhancement/EnhancementUtils.kt # app/src/main/java/com/google/android/samples/socialite/ui/mediaenhancement/EnhancementViewModel.kt # app/src/main/java/com/google/android/samples/socialite/ui/mediaenhancement/ImageEnhancementScreen.kt
…ncement # Conflicts: # app/src/main/java/com/google/android/samples/socialite/ui/mediaenhancement/EnhancementUtils.kt
…ncement # Conflicts: # app/src/main/java/com/google/android/samples/socialite/ui/mediaenhancement/EnhancementUtils.kt
Add on-device AI image enhancement feature to enhance images on the Chat screen.
Major changes:
ImageEnhancementScreenandEnhancementViewModelto handle image enhancement features, including tonemapping, deblurring, and upscaling.EnhancementUtils.ktproviding coroutine-based wrappers for enhancement session management, bitmap processing, and module installation.Minor changes:
ChatMessagemodel to include a unique ID and add support for updating media URIs in theChatRepositoryandMessageDao.Main.ktandSocialiteNavigation.kt.