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