diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index af3534342..db0723757 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -63,6 +63,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/feature/interests/impl/build.gradle.kts b/feature/interests/impl/build.gradle.kts
index fedc7fba6..353a24f57 100644
--- a/feature/interests/impl/build.gradle.kts
+++ b/feature/interests/impl/build.gradle.kts
@@ -25,6 +25,7 @@ android {
}
dependencies {
+ implementation(libs.androidx.activity.compose)
implementation(projects.core.domain)
implementation(projects.feature.topic.api)
implementation(projects.feature.interests.api)
diff --git a/feature/interests/impl/src/main/AndroidManifest.xml b/feature/interests/impl/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..04610619f
--- /dev/null
+++ b/feature/interests/impl/src/main/AndroidManifest.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/TabContent.kt b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/TabContent.kt
index d8a09c8f0..4d2934880 100644
--- a/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/TabContent.kt
+++ b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/TabContent.kt
@@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.feature.interests.impl
+import android.content.Intent
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
@@ -33,6 +34,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.DraggableScrollbar
@@ -40,6 +42,8 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollba
import com.google.samples.apps.nowinandroid.core.designsystem.component.scrollbar.scrollbarState
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.ui.InterestsItem
+import com.google.samples.apps.nowinandroid.feature.interests.impl.filter.FilterActivity
+import com.google.samples.apps.nowinandroid.feature.interests.impl.filter.IO_CONNECT_WORKSHOP_TOPIC_NAME
@Composable
fun TopicsTabContent(
@@ -63,6 +67,22 @@ fun TopicsTabContent(
contentPadding = PaddingValues(vertical = 16.dp),
state = scrollableState,
) {
+
+ item(key = IO_CONNECT_WORKSHOP_TOPIC_NAME) {
+ val context = LocalContext.current
+ InterestsItem(
+ name = IO_CONNECT_WORKSHOP_TOPIC_NAME,
+ following = false,
+ description = "",
+ topicImageUrl = "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Performance.svg?alt=media&token=558fdf02-1918-4527-b13f-323db67e31cc",
+ onClick = {
+ context.startActivity(Intent(context, FilterActivity::class.java))
+ },
+ onFollowButtonClick = {},
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+
topics.forEach { followableTopic ->
val topicId = followableTopic.topic.id
item(key = topicId) {
@@ -86,8 +106,10 @@ fun TopicsTabContent(
}
}
}
+
+ val itemsAvailable = topics.size + 1
val scrollbarState = scrollableState.scrollbarState(
- itemsAvailable = topics.size,
+ itemsAvailable = itemsAvailable,
)
scrollableState.DraggableScrollbar(
modifier = Modifier
@@ -98,7 +120,7 @@ fun TopicsTabContent(
state = scrollbarState,
orientation = Orientation.Vertical,
onThumbMoved = scrollableState.rememberDraggableScroller(
- itemsAvailable = topics.size,
+ itemsAvailable = itemsAvailable,
),
)
}
diff --git a/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/filter/FilterActivity.kt b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/filter/FilterActivity.kt
new file mode 100644
index 000000000..d5b745b05
--- /dev/null
+++ b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/filter/FilterActivity.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2026 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.feature.interests.impl.filter
+
+import android.graphics.Bitmap
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
+
+internal const val IO_CONNECT_WORKSHOP_TOPIC_NAME = "IO connect Workshop"
+
+class FilterActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ NiaTheme {
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier.padding(innerPadding).padding(top = 40.dp, bottom = 16.dp, start = 16.dp, end = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "Apply Filters Use Case",
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onErrorContainer,
+ fontWeight = FontWeight.Bold
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ FilterScreen(
+ modifier = Modifier
+ .fillMaxSize()
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun FilterScreen(modifier: Modifier = Modifier) {
+ val context = LocalContext.current
+ var selectedFilter by remember { mutableStateOf(FILTERS.first()) }
+
+ val sourceBitmap = remember { decodeSourceBitmap(context) }
+
+ val previewBitmap = createFilteredBitmap(sourceBitmap, selectedFilter)
+
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+
+ Text(
+ text = "Preview: ${selectedFilter.name}",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Image(
+ bitmap = previewBitmap.asImageBitmap(),
+ contentDescription = "Filtered Preview (${selectedFilter.name})",
+ modifier = Modifier
+ .size(300.dp)
+ .clip(RoundedCornerShape(12.dp))
+ .background(Color.LightGray),
+ contentScale = ContentScale.Crop
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ items(FILTERS, key = { it.name }) { filter ->
+ val thumbnail = createFilteredBitmap(sourceBitmap, filter)
+ FilterThumbnail(
+ image = thumbnail.asImageBitmap(),
+ name = filter.name,
+ selected = filter == selectedFilter,
+ onClick = { selectedFilter = filter}
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ Button(
+ onClick = {
+ val sharableBitmap = createFilteredBitmap(sourceBitmap, selectedFilter)
+ shareBitmap(context, sharableBitmap, selectedFilter.name)
+ },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(text = "Share filtered Image")
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ }
+
+}
+
+@Composable
+fun FilterThumbnail(
+ image: ImageBitmap,
+ name: String,
+ selected: Boolean,
+ onClick: () -> Unit
+) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.clickable(onClick = onClick)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(84.dp)
+ .clip(RoundedCornerShape(8.dp))
+ .border(
+ width = if (selected) 3.dp else 1.dp,
+ color = if (selected) MaterialTheme.colorScheme.primary else Color.Gray,
+ shape = RoundedCornerShape(8.dp)
+ )
+ ) {
+ Image(
+ bitmap = image,
+ contentDescription = name,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
+ }
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = name,
+ style = MaterialTheme.typography.labelMedium,
+ fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/filter/FilterUtils.kt b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/filter/FilterUtils.kt
new file mode 100644
index 000000000..c6746d710
--- /dev/null
+++ b/feature/interests/impl/src/main/kotlin/com/google/samples/apps/nowinandroid/feature/interests/impl/filter/FilterUtils.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2026 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.samples.apps.nowinandroid.feature.interests.impl.filter
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.ColorMatrix
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.Paint
+import androidx.core.content.FileProvider
+import com.google.samples.apps.nowinandroid.feature.interests.impl.R
+import java.io.File
+import java.io.FileOutputStream
+
+class ImageFilter(
+ val name: String,
+ val matrix: FloatArray
+)
+
+val FILTERS = listOf(
+ ImageFilter(
+ "Original", floatArrayOf(
+ 1f, 0f, 0f, 0f, 0f,
+ 0f, 1f, 0f, 0f, 0f,
+ 0f, 0f, 1f, 0f, 0f,
+ 0f, 0f, 0f, 1f, 0f
+ )
+ ),
+ ImageFilter(
+ "Grayscale", floatArrayOf(
+ 0.299f, 0.587f, 0.114f, 0f, 0f,
+ 0.299f, 0.587f, 0.114f, 0f, 0f,
+ 0.299f, 0.587f, 0.114f, 0f, 0f,
+ 0f, 0f, 0f, 1f, 0f
+ )
+ ),
+ ImageFilter(
+ "Sepia", floatArrayOf(
+ 0.393f, 0.769f, 0.189f, 0f, 0f,
+ 0.349f, 0.686f, 0.168f, 0f, 0f,
+ 0.272f, 0.534f, 0.131f, 0f, 0f,
+ 0f, 0f, 0f, 1f, 0f
+ )
+ ),
+ ImageFilter(
+ "Invert", floatArrayOf(
+ -1f, 0f, 0f, 0f, 255f,
+ 0f, -1f, 0f, 0f, 255f,
+ 0f, 0f, -1f, 0f, 255f,
+ 0f, 0f, 0f, 1f, 0f
+ )
+ ),
+ ImageFilter(
+ "Warm", floatArrayOf(
+ 1.15f, 0f, 0f, 0f, 10f,
+ 0f, 1.05f, 0f, 0f, 5f,
+ 0f, 0f, 0.85f, 0f, 0f,
+ 0f, 0f, 0f, 1f, 0f
+ )
+ ),
+ ImageFilter(
+ "Cool", floatArrayOf(
+ 0.85f, 0f, 0f, 0f, 0f,
+ 0f, 1f, 0f, 0f, 0f,
+ 0f, 0f, 1.15f, 0f, 10f,
+ 0f, 0f, 0f, 1f, 0f
+ )
+ ),
+ ImageFilter(
+ "Contrast", floatArrayOf(
+ 1.5f, 0f, 0f, 0f, -64f,
+ 0f, 1.5f, 0f, 0f, -64f,
+ 0f, 0f, 1.5f, 0f, -64f,
+ 0f, 0f, 0f, 1f, 0f
+ )
+ )
+)
+
+
+fun decodeSourceBitmap(context: Context): Bitmap {
+ val options = BitmapFactory.Options().apply { inSampleSize = 4 }
+ return BitmapFactory.decodeResource(context.resources, R.drawable.large_image, options)
+}
+
+fun createFilteredBitmap(source: Bitmap, filter: ImageFilter): Bitmap {
+ val result = Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(result)
+ val paint = Paint(Paint.FILTER_BITMAP_FLAG).apply {
+ colorFilter = ColorMatrixColorFilter(ColorMatrix(filter.matrix))
+ }
+ canvas.drawBitmap(source, 0f, 0f, paint)
+ return result
+}
+
+fun shareBitmap(context: Context, bitmap: Bitmap, filterName: String) {
+ val shareDir = File(context.cacheDir, "shared_images").apply { mkdirs() }
+ val file = File(shareDir, "filtered_${filterName.lowercase()}.png")
+ FileOutputStream(file).use { stream ->
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
+ }
+
+ val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
+ val intent = Intent(Intent.ACTION_SEND).apply {
+ type = "image/png"
+ putExtra(Intent.EXTRA_STREAM, uri)
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ context.startActivity(Intent.createChooser(intent, "Share filtered image"))
+}
\ No newline at end of file
diff --git a/feature/interests/impl/src/main/res/drawable-nodpi/large_image.jpg b/feature/interests/impl/src/main/res/drawable-nodpi/large_image.jpg
new file mode 100644
index 000000000..75666724a
Binary files /dev/null and b/feature/interests/impl/src/main/res/drawable-nodpi/large_image.jpg differ
diff --git a/memory-before.hprof b/memory-before.hprof
new file mode 100644
index 000000000..811e271b3
Binary files /dev/null and b/memory-before.hprof differ