Skip to content

Commit 36abea6

Browse files
Fix: Implement JSON serialization for SDK Content objects
Resolves "Serializer for class 'Content' is not found" error by introducing Data Transfer Objects (DTOs) and mappers for handling the serialization of `com.google.ai.client.generativeai.type.Content` and its `Part` types (specifically `TextPart` and `ImagePart`) using `kotlinx.serialization`. Key changes: 1. **DTOs Created (`PhotoReasoningDtos.kt`):** * Defined `@Serializable` data classes: `ContentDto`, `PartDto` (sealed interface), `TextPartDto`, and `ImagePartDto`. * `ImagePartDto` stores image data as a Base64 encoded string. 2. **Image Utilities (`ImageUtils.kt`):** * Added `bitmapToBase64` and `base64ToBitmap` functions to handle Bitmap to Base64 (PNG format) string conversion and vice-versa. 3. **Mappers Implemented (`PhotoReasoningMappers.kt`):** * Created extension functions to map between SDK `Content`/`Part` types and their corresponding DTOs. * These mappers utilize `ImageUtils` for handling `ImagePart` bitmap data. 4. **ViewModel Update (`PhotoReasoningViewModel.kt`):** * In `sendMessageWithRetry`, SDK `Content` objects (for input and chat history) are now first mapped to their DTOs before being serialized to JSON using `Json.encodeToString()`. 5. **Service Update (`ScreenCaptureService.kt`):** * In the `ACTION_EXECUTE_AI_CALL` handler, incoming JSON strings are deserialized to `ContentDto` and `List<ContentDto>`. * These DTOs are then mapped back to SDK `Content` types before being used for the AI call. This ensures that `Content` objects, which may not be directly serializable by `kotlinx.serialization` if they are from an external SDK without `@Serializable` annotations, can be safely serialized for Intent extras and deserialized for use by the service. This fixes the runtime crash you previously encountered.
1 parent 6a52988 commit 36abea6

5 files changed

Lines changed: 161 additions & 8 deletions

File tree

app/src/main/kotlin/com/google/ai/sample/ScreenCaptureService.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import android.view.WindowManager
2727
import android.widget.Toast
2828
import com.google.ai.client.generativeai.GenerativeModel
2929
import com.google.ai.client.generativeai.type.Content
30+
import com.google.ai.sample.feature.multimodal.dtos.ContentDto
31+
import com.google.ai.sample.feature.multimodal.dtos.toSdk
3032
import kotlinx.coroutines.CoroutineScope
3133
import kotlinx.coroutines.Dispatchers
3234
import kotlinx.coroutines.SupervisorJob
@@ -183,10 +185,13 @@ class ScreenCaptureService : Service() {
183185
var responseText: String? = null
184186
var errorMessage: String? = null
185187
try {
186-
// Deserialize chat history and input content.
187-
// Assumes Content/List<Content> are @Serializable or custom serializers are configured for Json object.
188-
val chatHistory = Json.decodeFromString<List<Content>>(chatHistoryJson)
189-
val inputContent = Json.decodeFromString<Content>(inputContentJson)
188+
// Deserialize JSON to DTOs.
189+
val chatHistoryDtos = Json.decodeFromString<List<ContentDto>>(chatHistoryJson)
190+
val inputContentDto = Json.decodeFromString<ContentDto>(inputContentJson)
191+
192+
// Convert DTOs back to SDK types.
193+
val chatHistory = chatHistoryDtos.map { it.toSdk() } // Uses ContentDto.toSdk()
194+
val inputContent = inputContentDto.toSdk() // Uses ContentDto.toSdk()
190195

191196
// Create a GenerativeModel instance for this specific call.
192197
// This ensures the call uses the API key and model name provided by the ViewModel.
@@ -198,9 +203,9 @@ class ScreenCaptureService : Service() {
198203
)
199204

200205
// Start a new chat session with the provided history for this call.
201-
val tempChat = generativeModel.startChat(history = chatHistory)
206+
val tempChat = generativeModel.startChat(history = chatHistory) // Use the mapped SDK history
202207
Log.d(TAG, "Executing AI sendMessage with history size: ${chatHistory.size}")
203-
val aiResponse = tempChat.sendMessage(inputContent)
208+
val aiResponse = tempChat.sendMessage(inputContent) // Use the mapped SDK inputContent
204209
responseText = aiResponse.text
205210
Log.d(TAG, "AI call successful. Response text available: ${responseText != null}")
206211

app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningViewModel.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import com.google.ai.sample.util.CommandParser
3232
import com.google.ai.sample.util.SystemMessagePreferences
3333
import com.google.ai.sample.util.SystemMessageEntryPreferences // Added import
3434
import com.google.ai.sample.util.SystemMessageEntry // Added import
35+
import com.google.ai.sample.feature.multimodal.dtos.toDto // Added for DTO mapping
3536
import kotlinx.coroutines.Dispatchers
3637
import kotlinx.coroutines.flow.MutableStateFlow
3738
import kotlinx.serialization.encodeToString
@@ -374,8 +375,11 @@ class PhotoReasoningViewModel(
374375
// This assumes Content and List<Content> are @Serializable or have custom serializers.
375376
// Add @file:UseSerializers(ContentSerializer::class, PartSerializer::class etc.) if needed at top of file
376377
// Or create DTOs. For this subtask, we'll assume direct serialization is possible.
377-
val inputContentJson = Json.encodeToString(inputContent)
378-
val chatHistoryJson = Json.encodeToString(chat.history) // chat.history is List<Content>
378+
val inputContentDto = inputContent.toDto() // Use the mapper
379+
val chatHistoryDtos = chat.history.map { it.toDto() } // Use the mapper for each item
380+
381+
val inputContentJson = Json.encodeToString(inputContentDto)
382+
val chatHistoryJson = Json.encodeToString(chatHistoryDtos)
379383

380384
val serviceIntent = Intent(context, ScreenCaptureService::class.java).apply {
381385
action = ScreenCaptureService.ACTION_EXECUTE_AI_CALL
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.google.ai.sample.feature.multimodal.dtos
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
// Note: The Gemini SDK's Content object can have a nullable 'role'.
7+
// Parts are non-empty.
8+
9+
@Serializable
10+
data class ContentDto(
11+
val role: String? = null, // e.g., "user", "model", "function"
12+
val parts: List<PartDto>
13+
)
14+
15+
@Serializable
16+
sealed interface PartDto
17+
18+
@Serializable
19+
@SerialName("text") // This helps ensure the type is clearly identified in JSON if needed, good practice for sealed types
20+
data class TextPartDto(val text: String) : PartDto
21+
22+
@Serializable
23+
@SerialName("image")
24+
data class ImagePartDto(
25+
val base64Image: String,
26+
// While the SDK's ImagePart takes a Bitmap, mimeType isn't directly part of its constructor.
27+
// We'll assume PNG for now or make it configurable if needed by the SDK upon reconstruction.
28+
// For simplicity, we'll just store the image data. The SDK might infer mimetype on load,
29+
// or the model might not need it explicitly if the image format is standard.
30+
// Let's keep it simple: just the base64 string. The reconstruction to Bitmap won't need a mimeType.
31+
// The SDK's `image(bitmap)` builder doesn't take a mimeType.
32+
) : PartDto
33+
34+
// Placeholder for other Part types if they become necessary later.
35+
// Example:
36+
// @Serializable
37+
// @SerialName("blob")
38+
// data class BlobPartDto(
39+
// val mimeType: String,
40+
// val base64Data: String // Or ByteArray
41+
// ) : PartDto
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.google.ai.sample.feature.multimodal.dtos
2+
3+
import android.graphics.Bitmap // Required for SDK ImagePart
4+
import com.google.ai.sample.util.ImageUtils // Our Base64<->Bitmap utils
5+
import com.google.ai.client.generativeai.type.Content // SDK Content
6+
import com.google.ai.client.generativeai.type.Part // SDK Part
7+
import com.google.ai.client.generativeai.type.TextPart // SDK TextPart
8+
import com.google.ai.client.generativeai.type.ImagePart // SDK ImagePart
9+
import com.google.ai.client.generativeai.type.content // SDK content builder
10+
11+
// --- SDK to DTO Mappers ---
12+
13+
fun Part.toDto(): PartDto {
14+
return when (this) {
15+
is TextPart -> TextPartDto(text = this.text)
16+
is ImagePart -> {
17+
// Assuming this.image is a Bitmap. The SDK's ImagePart constructor takes a Bitmap.
18+
val base64Image = ImageUtils.bitmapToBase64(this.image)
19+
ImagePartDto(base64Image = base64Image)
20+
}
21+
// Add other SDK Part types here if they become relevant
22+
// e.g., is BlobPart -> BlobPartDto(...)
23+
else -> throw IllegalArgumentException("Unsupported SDK Part type for DTO conversion: ${this.javaClass.name}")
24+
}
25+
}
26+
27+
fun Content.toDto(): ContentDto {
28+
return ContentDto(
29+
role = this.role, // SDK Content has a nullable 'role' (String)
30+
parts = this.parts.map { it.toDto() } // parts is List<Part>
31+
)
32+
}
33+
34+
// --- DTO to SDK Mappers ---
35+
36+
fun PartDto.toSdk(): Part {
37+
return when (this) {
38+
is TextPartDto -> TextPart(text = this.text)
39+
is ImagePartDto -> {
40+
val bitmap: Bitmap? = ImageUtils.base64ToBitmap(this.base64Image)
41+
// The SDK's image builder part of `content { image(bitmap) }` expects a non-null Bitmap.
42+
// If bitmap is null (due to bad Base64), we might throw an error or return a placeholder/skip.
43+
// For now, let's throw an error, as a null bitmap in an ImagePart usually indicates a problem.
44+
bitmap?.let { ImagePart(it) }
45+
?: throw IllegalArgumentException("Failed to convert Base64 to Bitmap for ImagePartDto, or Base64 was invalid.")
46+
}
47+
// Add other PartDto types here
48+
// else -> throw IllegalArgumentException("Unsupported PartDto type for SDK conversion: ${this.javaClass.name}")
49+
}
50+
}
51+
52+
fun ContentDto.toSdk(): Content {
53+
// The SDK's `content` builder is convenient here.
54+
// It takes a role (optional) and a block to define parts.
55+
val sdkParts = this.parts.map { it.toSdk() }
56+
return content(this.role) {
57+
sdkParts.forEach { sdkPart ->
58+
when (sdkPart) {
59+
is TextPart -> text(sdkPart.text)
60+
is ImagePart -> image(sdkPart.image) // sdkPart.image is the Bitmap
61+
// Add other SDK Part types here if they become relevant
62+
else -> throw IllegalArgumentException("Unsupported SDK Part type during ContentDto.toSdk() parts mapping: ${sdkPart.javaClass.name}")
63+
}
64+
}
65+
}
66+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.google.ai.sample.util // Or your chosen utility package
2+
3+
import android.graphics.Bitmap
4+
import android.graphics.BitmapFactory
5+
import android.util.Base64
6+
import java.io.ByteArrayOutputStream
7+
8+
object ImageUtils {
9+
10+
fun bitmapToBase64(bitmap: Bitmap, quality: Int = 80): String {
11+
// Ensure quality is within a reasonable range (0-100)
12+
val safeQuality = quality.coerceIn(0, 100)
13+
val outputStream = ByteArrayOutputStream()
14+
// Compress format can be PNG for lossless (but larger) or JPEG for lossy (smaller)
15+
// PNG is generally safer for AI models if exact pixel data matters.
16+
// Let's use PNG as it's lossless and doesn't require quality for compression itself in the same way JPEG does.
17+
// For PNG, the 'quality' parameter is ignored by compress().
18+
// If JPEG was used: bitmap.compress(Bitmap.CompressFormat.JPEG, safeQuality, outputStream)
19+
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) // Quality is ignored for PNG
20+
val byteArray = outputStream.toByteArray()
21+
return Base64.encodeToString(byteArray, Base64.NO_WRAP) // NO_WRAP is important for compact string
22+
}
23+
24+
fun base64ToBitmap(base64String: String): Bitmap? {
25+
return try {
26+
val decodedBytes = Base64.decode(base64String, Base64.DEFAULT)
27+
BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
28+
} catch (e: IllegalArgumentException) {
29+
// Log error or handle - Base64 string might be invalid
30+
android.util.Log.e("ImageUtils", "Error decoding Base64 string to Bitmap: ${e.message}")
31+
null
32+
} catch (e: Exception) {
33+
android.util.Log.e("ImageUtils", "Unexpected error decoding Base64 string to Bitmap: ${e.message}")
34+
null
35+
}
36+
}
37+
}

0 commit comments

Comments
 (0)