|
15 | 15 | */ |
16 | 16 | package com.android.ai.samples.genai_image_description |
17 | 17 |
|
18 | | -import android.content.Context |
| 18 | +import android.app.Application |
19 | 19 | import android.net.Uri |
20 | 20 | import android.provider.MediaStore |
21 | 21 | import android.util.Log |
22 | | -import androidx.lifecycle.ViewModel |
| 22 | +import androidx.annotation.StringRes |
| 23 | +import androidx.lifecycle.AndroidViewModel |
23 | 24 | import androidx.lifecycle.viewModelScope |
24 | 25 | import com.android.ai.samples.geminimultimodal.R |
| 26 | +import com.google.mlkit.genai.common.DownloadCallback |
25 | 27 | import com.google.mlkit.genai.common.FeatureStatus |
| 28 | +import com.google.mlkit.genai.common.GenAiException |
26 | 29 | import com.google.mlkit.genai.imagedescription.ImageDescriber |
27 | 30 | import com.google.mlkit.genai.imagedescription.ImageDescriberOptions |
28 | 31 | import com.google.mlkit.genai.imagedescription.ImageDescription |
29 | 32 | import com.google.mlkit.genai.imagedescription.ImageDescriptionRequest |
30 | 33 | import javax.inject.Inject |
31 | 34 | import kotlinx.coroutines.flow.MutableStateFlow |
32 | 35 | import kotlinx.coroutines.flow.StateFlow |
| 36 | +import kotlinx.coroutines.flow.asStateFlow |
| 37 | +import kotlinx.coroutines.flow.update |
33 | 38 | import kotlinx.coroutines.guava.await |
34 | 39 | import kotlinx.coroutines.launch |
35 | 40 |
|
36 | | -class GenAIImageDescriptionViewModel @Inject constructor() : ViewModel() { |
37 | | - private val _resultGenerated = MutableStateFlow("") |
38 | | - val resultGenerated: StateFlow<String> = _resultGenerated |
| 41 | +sealed class GenAIImageDescriptionUiState { |
| 42 | + data object Initial : GenAIImageDescriptionUiState() |
| 43 | + data object CheckingFeatureStatus : GenAIImageDescriptionUiState() |
| 44 | + data class DownloadingFeature( |
| 45 | + val bytesToDownload: Long, |
| 46 | + val bytesDownloaded: Long, |
| 47 | + ) : GenAIImageDescriptionUiState() |
39 | 48 |
|
40 | | - private var imageDescriber: ImageDescriber? = null |
| 49 | + data class Generating(val partialOutput: String) : GenAIImageDescriptionUiState() |
| 50 | + data class Success(val generatedOutput: String) : GenAIImageDescriptionUiState() |
| 51 | + data class Error(@StringRes val errorMessageStringRes: Int) : GenAIImageDescriptionUiState() |
| 52 | +} |
| 53 | + |
| 54 | +class GenAIImageDescriptionViewModel @Inject constructor(val context: Application) : AndroidViewModel(context) { |
| 55 | + private val _uiState = MutableStateFlow<GenAIImageDescriptionUiState>(GenAIImageDescriptionUiState.Initial) |
| 56 | + val uiState: StateFlow<GenAIImageDescriptionUiState> = _uiState.asStateFlow() |
41 | 57 |
|
42 | | - fun getImageDescription(imageUri: Uri?, context: Context) { |
| 58 | + private var imageDescriber: ImageDescriber = ImageDescription.getClient( |
| 59 | + ImageDescriberOptions.builder(context).build(), |
| 60 | + ) |
| 61 | + |
| 62 | + fun getImageDescription(imageUri: Uri?) { |
43 | 63 | if (imageUri == null) { |
44 | | - _resultGenerated.value = |
45 | | - context.getString(R.string.genai_image_description_no_image_selected) |
| 64 | + _uiState.value = GenAIImageDescriptionUiState.Error(R.string.genai_image_description_no_image_selected) |
46 | 65 | return |
47 | 66 | } |
48 | 67 |
|
49 | | - val imageDescriberOptions = ImageDescriberOptions.builder(context).build() |
50 | | - imageDescriber = ImageDescription.getClient(imageDescriberOptions) |
51 | | - |
52 | 68 | viewModelScope.launch { |
53 | | - imageDescriber?.let { imageDescriber -> |
54 | | - var featureStatus = FeatureStatus.UNAVAILABLE |
55 | | - |
56 | | - try { |
57 | | - featureStatus = imageDescriber.checkFeatureStatus().await() |
58 | | - } catch (error: Exception) { |
59 | | - Log.e("GenAIImageDesc", "Error checking feature status", error) |
60 | | - } |
61 | | - |
62 | | - if (featureStatus == FeatureStatus.UNAVAILABLE) { |
63 | | - _resultGenerated.value = |
64 | | - context.getString(R.string.genai_image_description_not_available) |
65 | | - return@launch |
66 | | - } |
67 | | - |
68 | | - // If feature is downloadable, making an inference call will automatically start |
69 | | - // the downloading process. |
70 | | - // If feature is downloading, the inference request will automatically execute after |
71 | | - // the feature has been downloaded. |
72 | | - // Alternatively, you can call imageDescriber.downloadFeature() to monitor the |
73 | | - // progress of the download. |
74 | | - if (featureStatus == FeatureStatus.DOWNLOADABLE || |
75 | | - featureStatus == FeatureStatus.DOWNLOADING |
76 | | - ) { |
77 | | - _resultGenerated.value = |
78 | | - context.getString(R.string.genai_image_description_downloading) |
79 | | - } |
80 | | - |
81 | | - val bitmap = MediaStore.Images.Media.getBitmap(context.contentResolver, imageUri) |
82 | | - val request = ImageDescriptionRequest.builder(bitmap).build() |
83 | | - imageDescriber.runInference(request) { newText -> |
84 | | - if (_resultGenerated.value == |
85 | | - context.getString(R.string.genai_image_description_downloading) |
86 | | - ) { |
87 | | - clearGeneratedText() |
88 | | - } |
89 | | - _resultGenerated.value += newText |
90 | | - } |
| 69 | + var featureStatus = FeatureStatus.UNAVAILABLE |
| 70 | + |
| 71 | + try { |
| 72 | + _uiState.value = GenAIImageDescriptionUiState.CheckingFeatureStatus |
| 73 | + featureStatus = imageDescriber.checkFeatureStatus().await() |
| 74 | + } catch (error: Exception) { |
| 75 | + _uiState.value = GenAIImageDescriptionUiState.Error(R.string.image_desc_feature_check_fail) |
| 76 | + Log.e("GenAIImageDesc", "Error checking feature status", error) |
| 77 | + } |
| 78 | + |
| 79 | + if (featureStatus == FeatureStatus.UNAVAILABLE) { |
| 80 | + _uiState.value = GenAIImageDescriptionUiState.Error(R.string.genai_image_description_not_available) |
91 | 81 | return@launch |
92 | 82 | } |
| 83 | + |
| 84 | + if (featureStatus == FeatureStatus.DOWNLOADABLE || featureStatus == FeatureStatus.DOWNLOADING) { |
| 85 | + imageDescriber.downloadFeature( |
| 86 | + object : DownloadCallback { |
| 87 | + override fun onDownloadStarted(bytesToDownload: Long) { |
| 88 | + _uiState.value = GenAIImageDescriptionUiState.DownloadingFeature(bytesToDownload, 0) |
| 89 | + } |
| 90 | + |
| 91 | + override fun onDownloadProgress(bytesDownloaded: Long) { |
| 92 | + _uiState.update { |
| 93 | + (it as? GenAIImageDescriptionUiState.DownloadingFeature)?.copy(bytesDownloaded = bytesDownloaded) ?: it |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + override fun onDownloadCompleted() { |
| 98 | + viewModelScope.launch { |
| 99 | + generateImageDescription(imageUri) |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + override fun onDownloadFailed(exception: GenAiException) { |
| 104 | + Log.e("GenAIImageDesc", "Download failed", exception) |
| 105 | + _uiState.value = GenAIImageDescriptionUiState.Error(R.string.image_desc_download_failed) |
| 106 | + } |
| 107 | + }, |
| 108 | + ) |
| 109 | + } else { |
| 110 | + generateImageDescription(imageUri) |
| 111 | + } |
93 | 112 | } |
94 | 113 | } |
95 | 114 |
|
96 | | - fun clearGeneratedText() { |
97 | | - _resultGenerated.value = "" |
| 115 | + private suspend fun generateImageDescription(imageUri: Uri) { |
| 116 | + _uiState.value = GenAIImageDescriptionUiState.Generating("") |
| 117 | + val bitmap = MediaStore.Images.Media.getBitmap(context.contentResolver, imageUri) |
| 118 | + val request = ImageDescriptionRequest.builder(bitmap).build() |
| 119 | + |
| 120 | + imageDescriber.runInference(request) { newText -> |
| 121 | + _uiState.update { |
| 122 | + (it as? GenAIImageDescriptionUiState.Generating)?.copy(partialOutput = it.partialOutput + newText) ?: it |
| 123 | + } |
| 124 | + }.await() |
| 125 | + |
| 126 | + (_uiState.value as? GenAIImageDescriptionUiState.Generating)?.partialOutput?.let { generatedOutput -> |
| 127 | + _uiState.value = GenAIImageDescriptionUiState.Success(generatedOutput) |
| 128 | + } |
98 | 129 | } |
99 | 130 |
|
100 | 131 | override fun onCleared() { |
101 | | - imageDescriber?.close() |
| 132 | + imageDescriber.close() |
102 | 133 | } |
103 | 134 | } |
0 commit comments