Skip to content

Commit 0668adc

Browse files
Magic selfie module refactoring
1 parent 534386c commit 0668adc

File tree

8 files changed

+240
-112
lines changed

8 files changed

+240
-112
lines changed

ai-catalog/app/src/main/java/com/android/ai/catalog/ui/domain/SampleCatalog.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import com.android.ai.samples.genai_image_description.GenAIImageDescriptionScree
2626
import com.android.ai.samples.genai_summarization.GenAISummarizationScreen
2727
import com.android.ai.samples.genai_writing_assistance.GenAIWritingAssistanceScreen
2828
import com.android.ai.samples.imagen.ui.ImagenScreen
29-
import com.android.ai.samples.magicselfie.MagicSelfieScreen
29+
import com.android.ai.samples.magicselfie.ui.MagicSelfieScreen
3030

3131
val sampleCatalog = listOf<SampleCatalogItem>(
3232
SampleCatalogItem(
Lines changed: 24 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,11 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package com.android.ai.samples.magicselfie
16+
package com.android.ai.samples.magicselfie.data
1717

1818
import android.graphics.Bitmap
1919
import android.graphics.Canvas
2020
import android.graphics.Paint
21-
import androidx.lifecycle.LiveData
22-
import androidx.lifecycle.MutableLiveData
23-
import androidx.lifecycle.ViewModel
24-
import androidx.lifecycle.viewModelScope
2521
import com.google.firebase.Firebase
2622
import com.google.firebase.ai.ai
2723
import com.google.firebase.ai.type.GenerativeBackend
@@ -33,19 +29,13 @@ import com.google.mlkit.vision.common.InputImage
3329
import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation
3430
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions
3531
import javax.inject.Inject
32+
import javax.inject.Singleton
33+
import kotlin.coroutines.suspendCoroutine
3634
import kotlin.math.roundToInt
37-
import kotlinx.coroutines.flow.MutableStateFlow
38-
import kotlinx.coroutines.launch
39-
40-
@OptIn(PublicPreviewAPI::class)
41-
class MagicSelfieViewModel @Inject constructor() : ViewModel() {
42-
43-
private val _foregroundBitmap = MutableStateFlow<Bitmap?>(null)
44-
val foregroundBitmap: MutableStateFlow<Bitmap?> = _foregroundBitmap
45-
46-
private val _progress = MutableLiveData<String?>(null)
47-
val progress: LiveData<String?> = _progress
4835

36+
@Singleton
37+
class MagicSelfieRepository @Inject constructor() {
38+
@OptIn(PublicPreviewAPI::class)
4939
private val imagenModel = Firebase.ai(backend = GenerativeBackend.vertexAI()).imagenModel(
5040
modelName = "imagen-4.0-generate-preview-06-06",
5141
generationConfig = ImagenGenerationConfig(
@@ -61,36 +51,28 @@ class MagicSelfieViewModel @Inject constructor() : ViewModel() {
6151
.build(),
6252
)
6353

64-
fun createMagicSelfie(bitmap: Bitmap, prompt: String) {
54+
suspend fun generateForegroundBitmap(bitmap: Bitmap): Bitmap {
6555
val image = InputImage.fromBitmap(bitmap, 0)
66-
67-
_progress.value = "Removing selfie background..."
68-
69-
subjectSegmenter.process(image)
70-
.addOnSuccessListener {
71-
it.foregroundBitmap?.let {
72-
_foregroundBitmap.value = it
73-
generateBackground(prompt)
56+
return suspendCoroutine { continuation ->
57+
subjectSegmenter.process(image)
58+
.addOnSuccessListener {
59+
it.foregroundBitmap?.let { foregroundBitmap ->
60+
continuation.resumeWith(Result.success(foregroundBitmap))
61+
}
62+
}
63+
.addOnFailureListener {
64+
continuation.resumeWith(Result.failure(it))
7465
}
75-
}.addOnFailureListener {
76-
_progress.postValue("Something went wrong :(")
77-
}
66+
}
7867
}
7968

80-
private fun generateBackground(prompt: String) {
81-
_progress.value = "Generating new background..."
82-
83-
viewModelScope.launch {
84-
val imageResponse = imagenModel.generateImages(
85-
prompt = prompt,
86-
)
87-
val image = imageResponse.images.first()
88-
89-
val bitmapImage = image.asBitmap()
90-
91-
_foregroundBitmap.value = combineBitmaps(_foregroundBitmap.value!!, bitmapImage)
92-
_progress.postValue(null)
93-
}
69+
@OptIn(PublicPreviewAPI::class)
70+
suspend fun generateBackground(prompt: String): Bitmap {
71+
val imageResponse = imagenModel.generateImages(
72+
prompt = prompt,
73+
)
74+
val image = imageResponse.images.first()
75+
return image.asBitmap()
9476
}
9577

9678
fun combineBitmaps(foreground: Bitmap, background: Bitmap): Bitmap {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.android.ai.samples.magicselfie.ui
17+
18+
import android.graphics.Bitmap
19+
import android.graphics.Matrix
20+
import android.media.ExifInterface
21+
import java.io.File
22+
23+
fun rotateImageIfRequired(imageFile: File, bitmap: Bitmap): Bitmap {
24+
val ei = ExifInterface(imageFile.absolutePath)
25+
val orientation = ei.getAttributeInt(
26+
ExifInterface.TAG_ORIENTATION,
27+
ExifInterface.ORIENTATION_NORMAL,
28+
)
29+
30+
return when (orientation) {
31+
ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(bitmap, 90f)
32+
ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(bitmap, 180f)
33+
ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(bitmap, 270f)
34+
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> flipImage(bitmap, true, false)
35+
ExifInterface.ORIENTATION_FLIP_VERTICAL -> flipImage(bitmap, false, true)
36+
ExifInterface.ORIENTATION_TRANSPOSE -> flipImage(rotateImage(bitmap, 90f), true, false)
37+
ExifInterface.ORIENTATION_TRANSVERSE -> flipImage(rotateImage(bitmap, 270f), true, false)
38+
else -> bitmap
39+
}
40+
}
41+
42+
fun rotateImage(bitmap: Bitmap, degrees: Float): Bitmap {
43+
val matrix = Matrix()
44+
matrix.postRotate(degrees)
45+
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
46+
}
47+
48+
fun flipImage(bitmap: Bitmap, horizontal: Boolean, vertical: Boolean): Bitmap {
49+
val matrix = Matrix()
50+
val scaleX = if (horizontal) -1f else 1f
51+
val scaleY = if (vertical) -1f else 1f
52+
matrix.setScale(scaleX, scaleY)
53+
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
54+
}
Lines changed: 33 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,12 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package com.android.ai.samples.magicselfie
16+
package com.android.ai.samples.magicselfie.ui
1717

1818
import android.annotation.SuppressLint
1919
import android.app.Activity
20-
import android.content.Context
2120
import android.content.Intent
2221
import android.graphics.Bitmap
23-
import android.graphics.Matrix
24-
import android.media.ExifInterface
25-
import android.net.Uri
2622
import android.provider.MediaStore
2723
import androidx.activity.compose.rememberLauncherForActivityResult
2824
import androidx.activity.result.ActivityResult
@@ -35,13 +31,13 @@ import androidx.compose.foundation.layout.Spacer
3531
import androidx.compose.foundation.layout.fillMaxSize
3632
import androidx.compose.foundation.layout.fillMaxWidth
3733
import androidx.compose.foundation.layout.height
34+
import androidx.compose.foundation.layout.imePadding
3835
import androidx.compose.foundation.layout.padding
3936
import androidx.compose.foundation.layout.size
4037
import androidx.compose.foundation.rememberScrollState
4138
import androidx.compose.foundation.verticalScroll
4239
import androidx.compose.material.icons.Icons
4340
import androidx.compose.material.icons.filled.CameraAlt
44-
import androidx.compose.material.icons.filled.Code
4541
import androidx.compose.material.icons.filled.SmartToy
4642
import androidx.compose.material3.Button
4743
import androidx.compose.material3.Card
@@ -58,7 +54,6 @@ import androidx.compose.material3.rememberTopAppBarState
5854
import androidx.compose.runtime.Composable
5955
import androidx.compose.runtime.collectAsState
6056
import androidx.compose.runtime.getValue
61-
import androidx.compose.runtime.livedata.observeAsState
6257
import androidx.compose.runtime.mutableStateOf
6358
import androidx.compose.runtime.remember
6459
import androidx.compose.runtime.setValue
@@ -69,16 +64,17 @@ import androidx.compose.ui.layout.ContentScale
6964
import androidx.compose.ui.platform.LocalContext
7065
import androidx.compose.ui.res.stringResource
7166
import androidx.compose.ui.unit.dp
72-
import androidx.compose.ui.unit.sp
7367
import androidx.core.content.FileProvider
7468
import androidx.hilt.navigation.compose.hiltViewModel
69+
import com.android.ai.samples.magicselfie.R
7570
import java.io.File
7671

7772
@OptIn(ExperimentalMaterial3Api::class)
7873
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
7974
@Composable
8075
fun MagicSelfieScreen(viewModel: MagicSelfieViewModel = hiltViewModel()) {
8176
val context = LocalContext.current
77+
val uiState by viewModel.uiState.collectAsState()
8278

8379
val topAppBarState = rememberTopAppBarState()
8480
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topAppBarState)
@@ -95,8 +91,6 @@ fun MagicSelfieScreen(viewModel: MagicSelfieViewModel = hiltViewModel()) {
9591
cameraIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
9692

9793
var selfieBitmap by remember { mutableStateOf<Bitmap?>(null) }
98-
val progress by viewModel.progress.observeAsState(null)
99-
val generatedBitmap by viewModel.foregroundBitmap.collectAsState()
10094
var editTextValue by remember { mutableStateOf("A very scenic view from the edge of the grand canyon") }
10195

10296
val resultLauncher =
@@ -132,6 +126,7 @@ fun MagicSelfieScreen(viewModel: MagicSelfieViewModel = hiltViewModel()) {
132126
Modifier
133127
.padding(12.dp)
134128
.padding(innerPadding)
129+
.imePadding()
135130
.verticalScroll(rememberScrollState()),
136131
) {
137132
Card(
@@ -141,12 +136,12 @@ fun MagicSelfieScreen(viewModel: MagicSelfieViewModel = hiltViewModel()) {
141136
height = 450.dp,
142137
),
143138
) {
144-
145-
if (generatedBitmap != null) {
139+
if (uiState is MagicSelfieUiState.Success) {
140+
val successState = uiState as MagicSelfieUiState.Success
146141
Image(
147-
bitmap = generatedBitmap!!.asImageBitmap(),
142+
bitmap = successState.bitmap.asImageBitmap(),
148143
contentDescription = "Picture",
149-
contentScale = ContentScale.Fit,
144+
contentScale = ContentScale.Crop,
150145
modifier = Modifier.fillMaxSize(),
151146
)
152147
} else if (selfieBitmap != null) {
@@ -183,74 +178,43 @@ fun MagicSelfieScreen(viewModel: MagicSelfieViewModel = hiltViewModel()) {
183178
viewModel.createMagicSelfie(selfieBitmap!!, editTextValue)
184179
}
185180
},
186-
enabled = progress == null,
181+
enabled = (uiState !is MagicSelfieUiState.RemovingBackground) &&
182+
(uiState !is MagicSelfieUiState.GeneratingBackground),
187183
) {
188184
Icon(Icons.Default.SmartToy, contentDescription = "Robot")
189185
Text(modifier = Modifier.padding(start = 8.dp), text = "Generate")
190186
}
191187

192-
if (progress != null) {
188+
if (uiState is MagicSelfieUiState.RemovingBackground) {
193189
Spacer(
194190
modifier = Modifier
195191
.height(30.dp)
196192
.padding(12.dp),
197193
)
198194
Text(
199-
text = progress!!,
195+
text = stringResource(R.string.removing_background),
196+
)
197+
} else if (uiState is MagicSelfieUiState.GeneratingBackground) {
198+
Spacer(
199+
modifier = Modifier
200+
.height(30.dp)
201+
.padding(12.dp),
202+
)
203+
Text(
204+
text = stringResource(R.string.generating_new_background),
205+
)
206+
} else if (uiState is MagicSelfieUiState.Error) {
207+
val errorState = uiState as MagicSelfieUiState.Error
208+
Spacer(
209+
modifier = Modifier
210+
.height(30.dp)
211+
.padding(12.dp),
212+
)
213+
Text(
214+
text = errorState.message ?: stringResource(R.string.unknown_error),
215+
color = MaterialTheme.colorScheme.error,
200216
)
201217
}
202218
}
203219
}
204220
}
205-
206-
fun rotateImageIfRequired(imageFile: File, bitmap: Bitmap): Bitmap {
207-
val ei = ExifInterface(imageFile.absolutePath)
208-
val orientation = ei.getAttributeInt(
209-
ExifInterface.TAG_ORIENTATION,
210-
ExifInterface.ORIENTATION_NORMAL,
211-
)
212-
213-
return when (orientation) {
214-
ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(bitmap, 90f)
215-
ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(bitmap, 180f)
216-
ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(bitmap, 270f)
217-
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> flipImage(bitmap, true, false)
218-
ExifInterface.ORIENTATION_FLIP_VERTICAL -> flipImage(bitmap, false, true)
219-
ExifInterface.ORIENTATION_TRANSPOSE -> flipImage(rotateImage(bitmap, 90f), true, false)
220-
ExifInterface.ORIENTATION_TRANSVERSE -> flipImage(rotateImage(bitmap, 270f), true, false)
221-
else -> bitmap
222-
}
223-
}
224-
225-
fun rotateImage(bitmap: Bitmap, degrees: Float): Bitmap {
226-
val matrix = Matrix()
227-
matrix.postRotate(degrees)
228-
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
229-
}
230-
231-
fun flipImage(bitmap: Bitmap, horizontal: Boolean, vertical: Boolean): Bitmap {
232-
val matrix = Matrix()
233-
val scaleX = if (horizontal) -1f else 1f
234-
val scaleY = if (vertical) -1f else 1f
235-
matrix.setScale(scaleX, scaleY)
236-
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
237-
}
238-
239-
@Composable
240-
fun SeeCodeButton(context: Context) {
241-
val githubLink = "https://github.com/android/ai-samples/tree/main/ai-catalog/samples/magic-selfie"
242-
Button(
243-
onClick = {
244-
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubLink))
245-
context.startActivity(intent)
246-
},
247-
modifier = Modifier.padding(end = 8.dp),
248-
) {
249-
Icon(Icons.Filled.Code, contentDescription = "See code")
250-
Text(
251-
modifier = Modifier.padding(start = 8.dp),
252-
fontSize = 12.sp,
253-
text = stringResource(R.string.see_code),
254-
)
255-
}
256-
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2025 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.android.ai.samples.magicselfie.ui
17+
18+
import android.graphics.Bitmap
19+
20+
sealed interface MagicSelfieUiState {
21+
data object Initial : MagicSelfieUiState
22+
data object RemovingBackground : MagicSelfieUiState
23+
data object GeneratingBackground : MagicSelfieUiState
24+
data class Success(val bitmap: Bitmap) : MagicSelfieUiState
25+
data class Error(val message: String?) : MagicSelfieUiState
26+
}

0 commit comments

Comments
 (0)