Skip to content

Commit 14e0b75

Browse files
authored
Merge pull request #93 from android/feature/sticker
Add an option to create a Sticker of the Android
2 parents 2aefcd2 + ccf6a04 commit 14e0b75

File tree

24 files changed

+461
-150
lines changed

24 files changed

+461
-150
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
<?xml version="1.0" encoding="utf-8"?>
2-
<!--
1+
<?xml version="1.0" encoding="utf-8"?><!--
32
Copyright 2025 The Android Open Source Project
43
54
Licensed under the Apache License, Version 2.0 (the "License");
@@ -66,6 +65,7 @@
6665
android:name="com.android.developers.androidify.startup.FirebaseRemoteConfigInitializer"
6766
android:value="@string/androidx_startup" />
6867
</provider>
68+
6969
<activity
7070
android:name=".MainActivity"
7171
android:configChanges="keyboard|keyboardHidden|screenSize|screenLayout"
@@ -87,6 +87,10 @@
8787
<activity
8888
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
8989
android:theme="@style/AppCompatAndroidify" />
90+
91+
<meta-data
92+
android:name="com.google.mlkit.vision.DEPENDENCIES"
93+
android:value="subject_segmentation" />
9094
</application>
9195

9296
</manifest>

benchmark/src/main/kotlin/com/android/developers/androidify/baselineprofile/BaselineProfileGenerator.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ class BaselineProfileGenerator {
4646
uiAutomator {
4747
startApp(packageName = packageName)
4848
onElement { textAsString() == "Let's Go" }.click()
49-
onElement{ textAsString() == "Prompt" }.click()
50-
onElement{ isEditable }.apply {
49+
onElement { textAsString() == "Prompt" }.click()
50+
onElement { isEditable }.apply {
5151
click()
5252
text =
5353
"wearing brown sneakers, a red t-shirt, " +

core/network/build.gradle.kts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,22 @@ dependencies {
7575
implementation(libs.firebase.analytics) {
7676
exclude(group = "com.google.guava")
7777
}
78+
7879
implementation(libs.firebase.app.check)
7980
implementation(libs.firebase.config)
8081
implementation(projects.core.util)
8182
implementation(libs.firebase.config.ktx)
83+
implementation(libs.mlkit.segmentation)
84+
implementation(libs.mlkit.common)
85+
implementation(libs.play.services.base)
8286
ksp(libs.hilt.compiler)
8387

88+
testImplementation(libs.play.services.base.testing)
89+
testImplementation(libs.junit)
90+
testImplementation(libs.kotlinx.coroutines.test)
91+
testImplementation(libs.robolectric)
92+
testImplementation(libs.androidx.core)
93+
8494
androidTestImplementation(libs.androidx.ui.test.junit4)
8595
androidTestImplementation(libs.hilt.android.testing)
8696
androidTestImplementation(projects.core.testing)

core/network/src/main/java/com/android/developers/androidify/di/NetworkModule.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import coil3.network.okhttp.OkHttpNetworkFetcherFactory
2424
import coil3.request.CachePolicy
2525
import coil3.request.crossfade
2626
import com.android.developers.androidify.network.BuildConfig
27+
import com.android.developers.androidify.ondevice.LocalSegmentationDataSource
28+
import com.android.developers.androidify.ondevice.LocalSegmentationDataSourceImpl
29+
import com.google.android.gms.common.moduleinstall.ModuleInstallClient
2730
import dagger.Module
2831
import dagger.Provides
2932
import dagger.hilt.InstallIn
@@ -92,6 +95,10 @@ internal class NetworkModule @Inject constructor() {
9295
.crossfade(true)
9396
.build()
9497

98+
@Provides
99+
fun segmentationDataSource(moduleInstallClient: ModuleInstallClient): LocalSegmentationDataSource {
100+
return LocalSegmentationDataSourceImpl(moduleInstallClient)
101+
}
95102
companion object {
96103
private const val TIMEOUT_SECONDS: Long = 120
97104
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.developers.androidify.di
17+
18+
import android.content.Context
19+
import com.google.android.gms.common.moduleinstall.ModuleInstall
20+
import com.google.android.gms.common.moduleinstall.ModuleInstallClient
21+
import dagger.Module
22+
import dagger.Provides
23+
import dagger.hilt.InstallIn
24+
import dagger.hilt.android.qualifiers.ApplicationContext
25+
import dagger.hilt.components.SingletonComponent
26+
27+
@Module
28+
@InstallIn(SingletonComponent::class)
29+
object OnDeviceModule {
30+
@Provides
31+
fun provideModuleInstallClient(@ApplicationContext context: Context): ModuleInstallClient {
32+
return ModuleInstall.getClient(context)
33+
}
34+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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.developers.androidify.ondevice
17+
18+
import android.graphics.Bitmap
19+
import android.util.Log
20+
import com.google.android.gms.common.moduleinstall.InstallStatusListener
21+
import com.google.android.gms.common.moduleinstall.ModuleInstallClient
22+
import com.google.android.gms.common.moduleinstall.ModuleInstallRequest
23+
import com.google.android.gms.common.moduleinstall.ModuleInstallStatusUpdate
24+
import com.google.android.gms.common.moduleinstall.ModuleInstallStatusUpdate.InstallState.STATE_CANCELED
25+
import com.google.android.gms.common.moduleinstall.ModuleInstallStatusUpdate.InstallState.STATE_FAILED
26+
import com.google.mlkit.vision.common.InputImage
27+
import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation
28+
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions
29+
import kotlinx.coroutines.CancellableContinuation
30+
import kotlinx.coroutines.suspendCancellableCoroutine
31+
import javax.inject.Inject
32+
import kotlin.coroutines.resume
33+
import kotlin.coroutines.resumeWithException
34+
35+
interface LocalSegmentationDataSource {
36+
suspend fun removeBackground(bitmap: Bitmap): Bitmap
37+
}
38+
39+
class LocalSegmentationDataSourceImpl @Inject constructor(
40+
private val moduleInstallClient: ModuleInstallClient
41+
) : LocalSegmentationDataSource {
42+
private val segmenter by lazy {
43+
val options = SubjectSegmenterOptions.Builder()
44+
.enableForegroundBitmap()
45+
.build()
46+
SubjectSegmentation.getClient(options)
47+
}
48+
49+
private suspend fun isSubjectSegmentationModuleInstalled(): Boolean {
50+
val areModulesAvailable =
51+
suspendCancellableCoroutine { continuation ->
52+
moduleInstallClient.areModulesAvailable(segmenter)
53+
.addOnSuccessListener {
54+
continuation.resume(it.areModulesAvailable())
55+
}
56+
.addOnFailureListener {
57+
continuation.resumeWithException(it)
58+
}
59+
}
60+
return areModulesAvailable
61+
}
62+
private class CustomInstallStatusListener(
63+
val continuation: CancellableContinuation<Boolean>
64+
) : InstallStatusListener {
65+
66+
override fun onInstallStatusUpdated(update: ModuleInstallStatusUpdate) {
67+
Log.d("LocalSegmentationDataSource", "Download progress: ${update.installState}.. ${continuation.hashCode()} ${continuation.isActive}")
68+
if (!continuation.isActive) return
69+
if (update.installState == ModuleInstallStatusUpdate.InstallState.STATE_COMPLETED) {
70+
continuation.resume(true)
71+
} else if (update.installState == STATE_FAILED || update.installState == STATE_CANCELED) {
72+
continuation.resumeWithException(
73+
ImageSegmentationException("Module download failed or was canceled. State: ${update.installState}")
74+
)
75+
} else {
76+
Log.d("LocalSegmentationDataSource", "State update: ${update.installState}")
77+
}
78+
}
79+
}
80+
private suspend fun installSubjectSegmentationModule(): Boolean {
81+
val result = suspendCancellableCoroutine { continuation ->
82+
val listener = CustomInstallStatusListener(continuation)
83+
val moduleInstallRequest = ModuleInstallRequest.newBuilder()
84+
.addApi(segmenter)
85+
.setListener(listener)
86+
.build()
87+
88+
moduleInstallClient
89+
.installModules(moduleInstallRequest)
90+
.addOnFailureListener {
91+
Log.e("LocalSegmentationDataSource", "Failed to download module", it)
92+
continuation.resumeWithException(it)
93+
}
94+
.addOnCompleteListener {
95+
Log.d("LocalSegmentationDataSource", "Successfully triggered download - await download progress updates")
96+
}
97+
}
98+
return result
99+
}
100+
101+
override suspend fun removeBackground(bitmap: Bitmap): Bitmap {
102+
val areModulesAvailable = isSubjectSegmentationModuleInstalled()
103+
104+
if (!areModulesAvailable) {
105+
Log.d("LocalSegmentationDataSource", "Modules not available - downloading")
106+
val result = installSubjectSegmentationModule()
107+
if (!result) {
108+
throw Exception("Failed to download module")
109+
}
110+
} else {
111+
Log.d("LocalSegmentationDataSource", "Modules available")
112+
}
113+
val image = InputImage.fromBitmap(bitmap, 0)
114+
return suspendCancellableCoroutine { continuation ->
115+
segmenter.process(image)
116+
.addOnSuccessListener { result ->
117+
if (result.foregroundBitmap != null) {
118+
continuation.resume(result.foregroundBitmap!!)
119+
} else {
120+
continuation.resumeWithException(ImageSegmentationException("Subject not found"))
121+
}
122+
}
123+
.addOnFailureListener { e ->
124+
Log.e("LocalSegmentationDataSource", "Exception while executing background removal", e)
125+
continuation.resumeWithException(e)
126+
}
127+
}
128+
}
129+
}
130+
131+
class ImageSegmentationException(message: String? = null): Exception(message)

core/network/src/main/java/com/android/developers/androidify/startup/FirebaseAppCheckInitializer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ package com.android.developers.androidify.startup
1818
import android.annotation.SuppressLint
1919
import android.content.Context
2020
import androidx.startup.Initializer
21+
import com.google.firebase.Firebase
2122
import com.google.firebase.appcheck.FirebaseAppCheck
2223
import com.google.firebase.appcheck.appCheck
2324
import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory
24-
import com.google.firebase.Firebase
2525

2626
/**
2727
* Initialize [FirebaseAppCheck] using the App Startup Library.

core/testing/src/main/java/com/android/developers/testing/repository/FakeImageGenerationRepository.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,8 @@ class FakeImageGenerationRepository : ImageGenerationRepository {
6464
): Bitmap {
6565
return createBitmap(1, 1)
6666
}
67+
68+
override suspend fun removeBackground(image: Bitmap): Bitmap {
69+
return createBitmap(1, 1)
70+
}
6771
}

core/theme/src/main/java/com/android/developers/androidify/theme/components/Backgrounds.kt

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ import com.android.developers.androidify.theme.AndroidifyTheme
4242
import com.android.developers.androidify.theme.R
4343
import com.android.developers.androidify.util.LargeScreensPreview
4444
import com.android.developers.androidify.util.PhonePreview
45-
import com.android.developers.androidify.util.backgroundRepeatX
4645
import com.android.developers.androidify.util.dpToPx
4746
import com.android.developers.androidify.util.isAtLeastMedium
4847

@@ -79,28 +78,6 @@ fun SquiggleBackground(
7978
}
8079
}
8180

82-
@Composable
83-
fun ScallopBackground(modifier: Modifier = Modifier) {
84-
val vectorBackground =
85-
rememberVectorPainter(ImageVector.vectorResource(R.drawable.shape_home_bg))
86-
val backgroundWidth = 300.dp
87-
BoxWithConstraints(
88-
modifier = modifier
89-
.fillMaxSize()
90-
.background(MaterialTheme.colorScheme.secondary),
91-
) {
92-
val maxHeight = this@BoxWithConstraints.maxHeight.dpToPx()
93-
Box(
94-
modifier = Modifier
95-
.fillMaxSize()
96-
.offset {
97-
IntOffset(0, y = (maxHeight * 0.6f).toInt())
98-
}
99-
.backgroundRepeatX(vectorBackground, backgroundWidth.dpToPx()),
100-
)
101-
}
102-
}
103-
10481
@LargeScreensPreview
10582
@Composable
10683
private fun SquiggleBackgroundLargePreview() {

data/src/main/java/com/android/developers/androidify/data/ImageGenerationRepository.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import android.util.Log
2222
import com.android.developers.androidify.RemoteConfigDataSource
2323
import com.android.developers.androidify.model.ValidatedDescription
2424
import com.android.developers.androidify.model.ValidatedImage
25+
import com.android.developers.androidify.ondevice.LocalSegmentationDataSource
2526
import com.android.developers.androidify.util.LocalFileProvider
2627
import com.android.developers.androidify.vertexai.FirebaseAiDataSource
2728
import java.io.File
@@ -38,6 +39,7 @@ interface ImageGenerationRepository {
3839
suspend fun saveImageToExternalStorage(imageUri: Uri): Uri
3940

4041
suspend fun addBackgroundToBot(image: Bitmap, backgroundPrompt: String) : Bitmap
42+
suspend fun removeBackground(image: Bitmap): Bitmap
4143
}
4244

4345
@Singleton
@@ -46,7 +48,8 @@ internal class ImageGenerationRepositoryImpl @Inject constructor(
4648
private val internetConnectivityManager: InternetConnectivityManager,
4749
private val geminiNanoDataSource: GeminiNanoGenerationDataSource,
4850
private val firebaseAiDataSource: FirebaseAiDataSource,
49-
private val remoteConfigDataSource: RemoteConfigDataSource
51+
private val remoteConfigDataSource: RemoteConfigDataSource,
52+
private val localSegmentationDataSource: LocalSegmentationDataSource,
5053
) : ImageGenerationRepository {
5154

5255
override suspend fun initialize() {
@@ -134,4 +137,8 @@ internal class ImageGenerationRepositoryImpl @Inject constructor(
134137
"\"" + backgroundPrompt + "\""
135138
return firebaseAiDataSource.generateImageWithEdit(image, backgroundBotInstructions)
136139
}
140+
141+
override suspend fun removeBackground(image: Bitmap): Bitmap {
142+
return localSegmentationDataSource.removeBackground(image)
143+
}
137144
}

0 commit comments

Comments
 (0)