diff options
Diffstat (limited to 'java')
13 files changed, 118 insertions, 73 deletions
diff --git a/java/res/drawable/inset_resolver_profile_tab_bg.xml b/java/res/drawable/inset_resolver_profile_tab_bg.xml new file mode 100644 index 00000000..bc62b047 --- /dev/null +++ b/java/res/drawable/inset_resolver_profile_tab_bg.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 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 + ~ + ~ http://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. + --> +<inset xmlns:android="http://schemas.android.com/apk/res/android" + android:drawable="@drawable/resolver_profile_tab_bg" + android:insetLeft="0dp" + android:insetRight="0dp" + android:insetTop="6dp" + android:insetBottom="6dp" /> diff --git a/java/res/layout/chooser_action_view.xml b/java/res/layout/chooser_action_view.xml index d045a7e3..6177821a 100644 --- a/java/res/layout/chooser_action_view.xml +++ b/java/res/layout/chooser_action_view.xml @@ -28,4 +28,4 @@ android:gravity="center" android:maxLines="1" android:textColor="?androidprv:attr/materialColorOnSurface" - android:textSize="12sp" /> + android:textSize="@dimen/chooser_action_view_text_size" /> diff --git a/java/res/layout/chooser_grid_item.xml b/java/res/layout/chooser_grid_item.xml index 18abc7bc..547a9944 100644 --- a/java/res/layout/chooser_grid_item.xml +++ b/java/res/layout/chooser_grid_item.xml @@ -51,14 +51,14 @@ android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceSmall" android:textColor="?androidprv:attr/materialColorOnSurface" - android:textSize="12sp" + android:textSize="@dimen/chooser_grid_target_name_text_size" android:maxLines="1" android:ellipsize="end" /> <!-- Activity name if set, gone for Direct Share targets --> <TextView android:id="@android:id/text2" android:textAppearance="?android:attr/textAppearanceSmall" - android:textSize="12sp" + android:textSize="@dimen/chooser_grid_activity_name_text_size" android:textColor="?androidprv:attr/materialColorOnSurfaceVariant" android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/java/res/layout/chooser_headline_row.xml b/java/res/layout/chooser_headline_row.xml index bfce7473..01be653f 100644 --- a/java/res/layout/chooser_headline_row.xml +++ b/java/res/layout/chooser_headline_row.xml @@ -35,7 +35,7 @@ app:layout_constrainedWidth="true" style="@style/TextAppearance.ChooserDefault" android:fontFamily="@androidprv:string/config_headlineFontFamily" - android:textSize="18sp" + android:textSize="@dimen/chooser_headline_text_size" /> <TextView diff --git a/java/res/layout/resolve_grid_item.xml b/java/res/layout/resolve_grid_item.xml index 25088773..e5a00429 100644 --- a/java/res/layout/resolve_grid_item.xml +++ b/java/res/layout/resolve_grid_item.xml @@ -50,7 +50,7 @@ android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceSmall" android:textColor="?androidprv:attr/materialColorOnSurface" - android:textSize="12sp" + android:textSize="@dimen/chooser_grid_target_name_text_size" android:gravity="top|center_horizontal" android:maxLines="1" android:ellipsize="end" /> @@ -58,7 +58,7 @@ <!-- Activity name if set, gone for Direct Share targets --> <TextView android:id="@android:id/text2" android:textAppearance="?android:attr/textAppearanceSmall" - android:textSize="12sp" + android:textSize="@dimen/chooser_grid_activity_name_text_size" android:textColor="?androidprv:attr/materialColorOnSurfaceVariant" android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/java/res/layout/resolver_profile_tab_button.xml b/java/res/layout/resolver_profile_tab_button.xml index 1c2bc1ca..52a1aacf 100644 --- a/java/res/layout/resolver_profile_tab_button.xml +++ b/java/res/layout/resolver_profile_tab_button.xml @@ -19,11 +19,10 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:layout_width="0dp" - android:layout_height="36dp" + android:layout_height="48dp" android:layout_weight="1" - android:layout_marginVertical="6dp" android:layout_marginHorizontal="@dimen/resolver_profile_tab_margin" - android:background="@drawable/resolver_profile_tab_bg" + android:background="@drawable/inset_resolver_profile_tab_bg" android:textColor="@color/resolver_profile_tab_text" android:textSize="@dimen/resolver_tab_text_size" android:textAppearance="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle" diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml index bd868c9f..a1f03276 100644 --- a/java/res/values/dimens.xml +++ b/java/res/values/dimens.xml @@ -34,6 +34,9 @@ <dimen name="chooser_max_collapsed_height">288dp</dimen> <dimen name="chooser_icon_size">56dp</dimen> <dimen name="chooser_badge_size">22dp</dimen> + <dimen name="chooser_headline_text_size">18sp</dimen> + <dimen name="chooser_grid_target_name_text_size">12sp</dimen> + <dimen name="chooser_grid_activity_name_text_size">12sp</dimen> <dimen name="resolver_icon_size">32dp</dimen> <dimen name="resolver_button_bar_spacing">0dp</dimen> <dimen name="resolver_badge_size">18dp</dimen> @@ -51,6 +54,7 @@ <dimen name="resolver_empty_state_container_padding_bottom">8dp</dimen> <dimen name="resolver_profile_tab_margin">4dp</dimen> <dimen name="chooser_action_view_icon_size">22dp</dimen> + <dimen name="chooser_action_view_text_size">12sp</dimen> <dimen name="chooser_action_margin">0dp</dimen> <dimen name="modify_share_text_toggle_max_width">150dp</dimen> <dimen name="chooser_view_spacing">16dp</dimen> diff --git a/java/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoader.kt index ce064cdf..2e2aa938 100644 --- a/java/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoader.kt +++ b/java/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoader.kt @@ -28,6 +28,7 @@ import javax.inject.Qualifier import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch @@ -104,6 +105,10 @@ constructor( // [CancellationExceptions]s so that they don't cancel the calling coroutine/scope. runCatching { cache[uri].await() }.getOrNull() + @OptIn(ExperimentalCoroutinesApi::class) + override fun getCachedBitmap(uri: Uri): Bitmap? = + kotlin.runCatching { cache[uri].getCompleted() }.getOrNull() + companion object { private const val TAG = "CachingImgPrevLoader" } diff --git a/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt index 629651a3..81913a8e 100644 --- a/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt +++ b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt @@ -35,6 +35,9 @@ interface ImageLoader : suspend (Uri) -> Bitmap?, suspend (Uri, Boolean) -> Bitm /** Prepopulate the image loader cache. */ fun prePopulate(uris: List<Uri>) + /** Returns a bitmap for the given URI if it's already cached, otherwise null */ + fun getCachedBitmap(uri: Uri): Bitmap? = null + /** Load preview image; caching is allowed. */ override suspend fun invoke(uri: Uri) = invoke(uri, true) diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt index 71d16da9..197d6858 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt @@ -40,28 +40,25 @@ fun ShareouselCard( image: @Composable () -> Unit, contentType: ContentType, selected: Boolean, - loadComplete: Boolean, modifier: Modifier = Modifier, ) { Box(modifier) { image() - if (loadComplete) { - val topButtonPadding = 12.dp - Box(modifier = Modifier.padding(topButtonPadding).matchParentSize()) { - SelectionIcon(selected, modifier = Modifier.align(Alignment.TopStart)) - when (contentType) { - ContentType.Video -> - TypeIcon( - R.drawable.ic_play_circle_filled_24px, - modifier = Modifier.align(Alignment.TopEnd) - ) - ContentType.Other -> - TypeIcon( - R.drawable.chooser_file_generic, - modifier = Modifier.align(Alignment.TopEnd) - ) - ContentType.Image -> Unit // No additional icon needed. - } + val topButtonPadding = 12.dp + Box(modifier = Modifier.padding(topButtonPadding).matchParentSize()) { + SelectionIcon(selected, modifier = Modifier.align(Alignment.TopStart)) + when (contentType) { + ContentType.Video -> + TypeIcon( + R.drawable.ic_play_circle_filled_24px, + modifier = Modifier.align(Alignment.TopEnd) + ) + ContentType.Other -> + TypeIcon( + R.drawable.chooser_file_generic, + modifier = Modifier.align(Alignment.TopEnd) + ) + ContentType.Image -> Unit // No additional icon needed. } } } diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt index a40a9c50..c63055d2 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt @@ -15,6 +15,7 @@ */ package com.android.intentresolver.contentpreview.payloadtoggle.ui.composable +import androidx.compose.animation.Crossfade import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -126,15 +127,14 @@ private fun PreviewCarousel( } } - ShareouselCard(viewModel.preview(model, previewIndex)) + ShareouselCard(viewModel.preview(model, previewIndex, rememberCoroutineScope())) } } } @Composable private fun ShareouselCard(viewModel: ShareouselPreviewViewModel) { - val bitmapLoadState by - viewModel.bitmapLoadState.collectAsStateWithLifecycle(initialValue = ValueUpdate.Absent) + val bitmapLoadState by viewModel.bitmapLoadState.collectAsStateWithLifecycle() val selected by viewModel.isSelected.collectAsStateWithLifecycle(initialValue = false) val borderColor = MaterialTheme.colorScheme.primary val scope = rememberCoroutineScope() @@ -144,47 +144,56 @@ private fun ShareouselCard(viewModel: ShareouselPreviewViewModel) { ContentType.Video -> stringResource(R.string.selectable_video) else -> stringResource(R.string.selectable_item) } - // Image load is complete (but may have failed) - val loadComplete = bitmapLoadState is ValueUpdate.Value - ShareouselCard( - image = { - // TODO: max ratio is actually equal to the viewport ratio - val aspectRatio = viewModel.aspectRatio.coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO) - bitmapLoadState.getOrDefault(null)?.let { bitmap -> - Image( - bitmap = bitmap.asImageBitmap(), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.aspectRatio(aspectRatio), - ) - } - ?: run { - // TODO: look at ScrollableImagePreviewView.setLoading() - Box( - modifier = - Modifier.fillMaxHeight() - .aspectRatio(aspectRatio) - .background(color = MaterialTheme.colorScheme.surfaceContainerHigh) - ) - } - }, - contentType = viewModel.contentType, - loadComplete = loadComplete, - selected = selected, + Crossfade( + targetState = bitmapLoadState, modifier = - Modifier.thenIf(selected && loadComplete) { - Modifier.border( - width = 4.dp, - color = borderColor, - shape = RoundedCornerShape(size = 12.dp), - ) - } - .semantics { this.contentDescription = contentDescription } + Modifier.semantics { this.contentDescription = contentDescription } .clip(RoundedCornerShape(size = 12.dp)) .toggleable( value = selected, onValueChange = { scope.launch { viewModel.setSelected(it) } }, ) + ) { state -> + // TODO: max ratio is actually equal to the viewport ratio + val aspectRatio = viewModel.aspectRatio.coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO) + if (state is ValueUpdate.Value) { + state.getOrDefault(null).let { bitmap -> + ShareouselCard( + image = { + bitmap?.let { + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.aspectRatio(aspectRatio), + ) + } ?: PlaceholderBox(aspectRatio) + }, + contentType = viewModel.contentType, + selected = selected, + modifier = + Modifier.thenIf(selected) { + Modifier.border( + width = 4.dp, + color = borderColor, + shape = RoundedCornerShape(size = 12.dp), + ) + } + ) + } + } else { + PlaceholderBox(aspectRatio) + } + } +} + +@Composable +private fun PlaceholderBox(aspectRatio: Float) { + Box( + modifier = + Modifier.fillMaxHeight() + .aspectRatio(aspectRatio) + .background(color = MaterialTheme.colorScheme.surfaceContainerHigh) ) } diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt index 1acdcf7a..de435290 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt @@ -20,11 +20,12 @@ import android.graphics.Bitmap import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate import com.android.intentresolver.contentpreview.payloadtoggle.shared.ContentType import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow /** An individual preview within Shareousel. */ data class ShareouselPreviewViewModel( /** Image to be shared. */ - val bitmapLoadState: Flow<ValueUpdate<Bitmap?>>, + val bitmapLoadState: StateFlow<ValueUpdate<Bitmap?>>, /** Type of data to be shared. */ val contentType: ContentType, /** Whether this preview has been selected by the user. */ diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt index 19acb318..d0b89860 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt @@ -56,7 +56,8 @@ data class ShareouselViewModel( /** List of action chips presented underneath Shareousel. */ val actions: Flow<List<ActionChipViewModel>>, /** Creates a [ShareouselPreviewViewModel] for a [PreviewModel] present in [previews]. */ - val preview: (key: PreviewModel, index: Int?) -> ShareouselPreviewViewModel, + val preview: + (key: PreviewModel, index: Int?, scope: CoroutineScope) -> ShareouselPreviewViewModel, ) @Module @@ -113,7 +114,7 @@ interface ShareouselViewModelModule { } } }, - preview = { key, index -> + preview = { key, index, previewScope -> keySet.value?.maybeLoad(index) val previewInteractor = interactor.preview(key) val contentType = @@ -122,14 +123,19 @@ interface ShareouselViewModelModule { mimeTypeClassifier.isVideoType(key.mimeType) -> ContentType.Video else -> ContentType.Other } + val initialBitmapValue = + key.previewUri?.let { + imageLoader.getCachedBitmap(it)?.let { ValueUpdate.Value(it) } + } ?: ValueUpdate.Absent ShareouselPreviewViewModel( bitmapLoadState = flow { - emit( - key.previewUri?.let { ValueUpdate.Value(imageLoader(it)) } - ?: ValueUpdate.Absent - ) - }, + emit( + key.previewUri?.let { ValueUpdate.Value(imageLoader(it)) } + ?: ValueUpdate.Absent + ) + } + .stateIn(previewScope, SharingStarted.Eagerly, initialBitmapValue), contentType = contentType, isSelected = previewInteractor.isSelected, setSelected = previewInteractor::setSelected, |