summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
author Andrey Yepin <ayepin@google.com> 2024-08-01 14:56:31 -0700
committer Andrey Yepin <ayepin@google.com> 2024-08-15 11:41:15 -0700
commit754d59681277b2afdd52b79746a593488f132e4b (patch)
tree73c52de4857c9f943bea1cf7524a422aa02fb1d6 /java
parent2f6086166956949bc5ab732795f671b1800cb613 (diff)
Allow toggling of final shareousel item
When no items selected the UI will: * change the chooser headline; * make the custom action row invisible; * make all targets disabled and all target icons greyscale. Fix: 349468879 Test: atest IntentResolver-tests-unit Test: manual functionality testing Flag: com.android.intentresolver.unselect_final_item Change-Id: I53b9c908943b1f1003cb0131a6dec8abc26ec782
Diffstat (limited to 'java')
-rw-r--r--java/res/values/strings.xml3
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java9
-rw-r--r--java/src/com/android/intentresolver/ChooserHelper.kt40
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java6
-rw-r--r--java/src/com/android/intentresolver/ResolverListAdapter.java18
-rw-r--r--java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt2
-rw-r--r--java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt3
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt4
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt9
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt58
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt16
11 files changed, 125 insertions, 43 deletions
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index c026ee59..4f77d248 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -162,6 +162,9 @@
}
</string>
+ <!-- Title atop a sharing UI indicating that a selection needs to be made for sharing -->
+ <string name="select_items_to_share">Select items to share</string>
+
<!-- Title atop a sharing UI indicating that some number of images are being shared
along with text [CHAR_LIMIT=50] -->
<string name="sharing_images_with_text">{count, plural,
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 51d8785f..8871ce3f 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -25,6 +25,7 @@ import static androidx.lifecycle.LifecycleKt.getCoroutineScope;
import static com.android.intentresolver.ChooserActionFactory.EDIT_SOURCE;
import static com.android.intentresolver.Flags.shareouselUpdateExcludeComponentsExtra;
import static com.android.intentresolver.Flags.fixShortcutsFlashing;
+import static com.android.intentresolver.Flags.unselectFinalItem;
import static com.android.intentresolver.ext.CreationExtrasExtKt.addDefaultArgs;
import static com.android.intentresolver.profiles.MultiProfilePagerAdapter.PROFILE_PERSONAL;
import static com.android.intentresolver.profiles.MultiProfilePagerAdapter.PROFILE_WORK;
@@ -351,6 +352,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
if (mChooserServiceFeatureFlags.chooserPayloadToggling()) {
mChooserHelper.setOnChooserRequestChanged(this::onChooserRequestChanged);
mChooserHelper.setOnPendingSelection(this::onPendingSelection);
+ if (unselectFinalItem()) {
+ mChooserHelper.setOnHasSelections(this::onHasSelections);
+ }
}
}
private int mInitialProfile = -1;
@@ -705,7 +709,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
}
private void onChooserRequestChanged(ChooserRequest chooserRequest) {
- // intentional reference comparison
if (mRequest == chooserRequest) {
return;
}
@@ -724,6 +727,10 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
setTabsViewEnabled(false);
}
+ private void onHasSelections(boolean hasSelections) {
+ mChooserMultiProfilePagerAdapter.setTargetsEnabled(hasSelections);
+ }
+
private void onAppTargetsLoaded(ResolverListAdapter listAdapter) {
Log.d(TAG, "onAppTargetsLoaded("
+ "listAdapter.userHandle=" + listAdapter.getUserHandle() + ")");
diff --git a/java/src/com/android/intentresolver/ChooserHelper.kt b/java/src/com/android/intentresolver/ChooserHelper.kt
index 312911a6..7d382bfc 100644
--- a/java/src/com/android/intentresolver/ChooserHelper.kt
+++ b/java/src/com/android/intentresolver/ChooserHelper.kt
@@ -27,9 +27,12 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
+import com.android.intentresolver.Flags.unselectFinalItem
import com.android.intentresolver.annotation.JavaInterop
+import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION
import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ActivityResultRepository
import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PendingSelectionCallbackRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository
import com.android.intentresolver.data.model.ChooserRequest
import com.android.intentresolver.platform.GlobalSettings
import com.android.intentresolver.ui.viewmodel.ChooserViewModel
@@ -39,6 +42,8 @@ import com.android.intentresolver.validation.log
import dagger.hilt.android.scopes.ActivityScoped
import java.util.function.Consumer
import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
@@ -46,6 +51,7 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
private const val TAG: String = "ChooserHelper"
@@ -86,6 +92,7 @@ constructor(
hostActivity: Activity,
private val activityResultRepo: ActivityResultRepository,
private val pendingSelectionCallbackRepo: PendingSelectionCallbackRepository,
+ private val selectionsRepo: PreviewSelectionsRepository,
private val globalSettings: GlobalSettings,
) : DefaultLifecycleObserver {
// This is guaranteed by Hilt, since only a ComponentActivity is injectable.
@@ -98,6 +105,7 @@ constructor(
var onChooserRequestChanged: Consumer<ChooserRequest> = Consumer {}
/** Invoked when there are a new change to payload selection */
var onPendingSelection: Runnable = Runnable {}
+ var onHasSelections: Consumer<Boolean> = Consumer {}
init {
activity.lifecycle.addObserver(this)
@@ -144,22 +152,40 @@ constructor(
}
activity.lifecycleScope.launch {
- val hasPendingCallbackFlow =
+ val hasPendingIntentFlow =
pendingSelectionCallbackRepo.pendingTargetIntent
.map { it != null }
.distinctUntilChanged()
- .onEach { hasPendingCallback ->
- if (hasPendingCallback) {
+ .onEach { hasPendingIntent ->
+ if (hasPendingIntent) {
onPendingSelection.run()
}
}
activity.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.request
- .combine(hasPendingCallbackFlow) { request, hasPendingCallback ->
- request to hasPendingCallback
+ val hasSelectionFlow =
+ if (
+ unselectFinalItem() &&
+ viewModel.previewDataProvider.previewType ==
+ CONTENT_PREVIEW_PAYLOAD_SELECTION
+ ) {
+ selectionsRepo.selections
+ .map { it.isNotEmpty() }
+ .distinctUntilChanged()
+ .stateIn(scope = this)
+ .also { flow -> launch { flow.collect { onHasSelections.accept(it) } } }
+ } else {
+ MutableStateFlow(true).asStateFlow()
}
+ val requestControlFlow =
+ hasSelectionFlow
+ .combine(hasPendingIntentFlow) { hasSelections, hasPendingIntent ->
+ hasSelections && !hasPendingIntent
+ }
+ .distinctUntilChanged()
+ viewModel.request
+ .combine(requestControlFlow) { request, isReady -> request to isReady }
// only take ChooserRequest if there are no pending callbacks
- .filter { !it.second }
+ .filter { it.second }
.map { it.first }
.distinctUntilChanged(areEquivalent = { old, new -> old === new })
.collect { onChooserRequestChanged.accept(it) }
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java
index 26e2feeb..016eb714 100644
--- a/java/src/com/android/intentresolver/ChooserListAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -18,7 +18,6 @@ package com.android.intentresolver;
import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
-import static com.android.intentresolver.util.graphics.SuspendedMatrixColorFilter.getSuspendedColorMatrix;
import android.app.ActivityManager;
import android.app.prediction.AppTarget;
@@ -439,10 +438,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
}
- holder.bindIcon(info);
- if (info.hasDisplayIcon() && !mTargetsEnabled) {
- holder.icon.setColorFilter(getSuspendedColorMatrix());
- }
+ holder.bindIcon(info, mTargetsEnabled);
if (mAnimateItems && info.hasDisplayIcon()) {
mAnimationTracker.animateIcon(holder.icon, info);
}
diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java
index 7ca1c724..fc5514b6 100644
--- a/java/src/com/android/intentresolver/ResolverListAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverListAdapter.java
@@ -16,6 +16,7 @@
package com.android.intentresolver;
+import static com.android.intentresolver.Flags.unselectFinalItem;
import static com.android.intentresolver.util.graphics.SuspendedMatrixColorFilter.getSuspendedColorMatrix;
import android.content.Context;
@@ -973,13 +974,26 @@ public class ResolverListAdapter extends BaseAdapter {
/**
* Bind view holder to a TargetInfo.
*/
- public void bindIcon(TargetInfo info) {
+ public final void bindIcon(TargetInfo info) {
+ bindIcon(info, true);
+ }
+
+ /**
+ * Bind view holder to a TargetInfo.
+ */
+ public void bindIcon(TargetInfo info, boolean isEnabled) {
Drawable displayIcon = info.getDisplayIconHolder().getDisplayIcon();
icon.setImageDrawable(displayIcon);
- if (info.isSuspended()) {
+ if (info.isSuspended() || !isEnabled) {
icon.setColorFilter(getSuspendedColorMatrix());
} else {
icon.setColorFilter(null);
+ if (unselectFinalItem() && displayIcon != null) {
+ // For some reason, ImageView.setColorFilter() not always propagate the call
+ // to the drawable and the icon remains grayscale when rebound; reset the filter
+ // explicitly.
+ displayIcon.setColorFilter(null);
+ }
}
}
}
diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt
index 21308341..059ee083 100644
--- a/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt
+++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt
@@ -36,4 +36,6 @@ interface HeadlineGenerator {
fun getVideosHeadline(count: Int): String
fun getFilesHeadline(count: Int): String
+
+ fun getNotItemsSelectedHeadline(): String
}
diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt
index e92d9bc6..822d3097 100644
--- a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt
+++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt
@@ -93,6 +93,9 @@ constructor(
return getPluralString(R.string.sharing_files, count)
}
+ override fun getNotItemsSelectedHeadline(): String =
+ context.getString(R.string.select_items_to_share)
+
private fun getPluralString(@StringRes templateResource: Int, count: Int): String {
return PluralsMessageFormatter.format(
context.resources,
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt
index 81c56d1e..0688ce02 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt
@@ -18,12 +18,12 @@ package com.android.intentresolver.contentpreview.payloadtoggle.data.repository
import android.net.Uri
import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
-import dagger.hilt.android.scopes.ViewModelScoped
+import dagger.hilt.android.scopes.ActivityRetainedScoped
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
/** Stores set of selected previews. */
-@ViewModelScoped
+@ActivityRetainedScoped
class PreviewSelectionsRepository @Inject constructor() {
val selections = MutableStateFlow(emptyMap<Uri, PreviewModel>())
}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt
index 97d9fa66..2d02e4fd 100644
--- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt
@@ -17,6 +17,7 @@
package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
import android.net.Uri
+import com.android.intentresolver.Flags.unselectFinalItem
import com.android.intentresolver.contentpreview.MimeTypeClassifier
import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository
import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.TargetIntentModifier
@@ -60,8 +61,12 @@ constructor(
}
fun unselect(model: PreviewModel) {
- if (selectionsRepo.selections.value.size > 1) {
- updateChooserRequest(selectionsRepo.selections.updateAndGet { it - model.uri }.values)
+ if (selectionsRepo.selections.value.size > 1 || unselectFinalItem()) {
+ selectionsRepo.selections
+ .updateAndGet { it - model.uri }
+ .values
+ .takeIf { it.isNotEmpty() }
+ ?.let { updateChooserRequest(it) }
}
}
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 93ac90db..f8cf243d 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
@@ -64,6 +64,7 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.intentresolver.Flags.unselectFinalItem
import com.android.intentresolver.R
import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate
import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.getOrDefault
@@ -73,6 +74,7 @@ import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.Shar
import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselViewModel
import kotlin.math.abs
import kotlin.math.min
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@Composable
@@ -286,30 +288,46 @@ private fun ActionCarousel(viewModel: ShareouselViewModel) {
val actions by viewModel.actions.collectAsStateWithLifecycle(initialValue = emptyList())
if (actions.isNotEmpty()) {
Spacer(Modifier.height(16.dp))
- LazyRow(
- horizontalArrangement = Arrangement.spacedBy(4.dp),
- modifier = Modifier.height(32.dp),
- ) {
- itemsIndexed(actions) { idx, actionViewModel ->
- if (idx == 0) {
- Spacer(Modifier.width(dimensionResource(R.dimen.chooser_edge_margin_normal)))
- }
- ShareouselAction(
- label = actionViewModel.label,
- onClick = { actionViewModel.onClicked() },
- ) {
- actionViewModel.icon?.let {
- Image(
- icon = it,
- modifier = Modifier.size(16.dp),
- colorFilter = ColorFilter.tint(LocalContentColor.current)
+ val visibilityFlow =
+ if (unselectFinalItem()) {
+ viewModel.hasSelectedItems
+ } else {
+ MutableStateFlow(true)
+ }
+ val visibility by visibilityFlow.collectAsStateWithLifecycle(true)
+ val height = 32.dp
+ if (visibility) {
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ modifier = Modifier.height(height),
+ ) {
+ itemsIndexed(actions) { idx, actionViewModel ->
+ if (idx == 0) {
+ Spacer(
+ Modifier.width(dimensionResource(R.dimen.chooser_edge_margin_normal))
+ )
+ }
+ ShareouselAction(
+ label = actionViewModel.label,
+ onClick = { actionViewModel.onClicked() },
+ ) {
+ actionViewModel.icon?.let {
+ Image(
+ icon = it,
+ modifier = Modifier.size(16.dp),
+ colorFilter = ColorFilter.tint(LocalContentColor.current)
+ )
+ }
+ }
+ if (idx == actions.size - 1) {
+ Spacer(
+ Modifier.width(dimensionResource(R.dimen.chooser_edge_margin_normal))
)
}
- }
- if (idx == actions.size - 1) {
- Spacer(Modifier.width(dimensionResource(R.dimen.chooser_edge_margin_normal)))
}
}
+ } else {
+ Spacer(modifier = Modifier.height(height))
}
}
}
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 6f8be1ff..9762794e 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
@@ -17,6 +17,7 @@ package com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel
import android.util.Size
import com.android.intentresolver.Flags
+import com.android.intentresolver.Flags.unselectFinalItem
import com.android.intentresolver.contentpreview.CachingImagePreviewImageLoader
import com.android.intentresolver.contentpreview.HeadlineGenerator
import com.android.intentresolver.contentpreview.ImageLoader
@@ -58,6 +59,8 @@ data class ShareouselViewModel(
val previews: Flow<PreviewsModel?>,
/** List of action chips presented underneath Shareousel. */
val actions: Flow<List<ActionChipViewModel>>,
+ /** Indicates whether there are any selected items */
+ val hasSelectedItems: Flow<Boolean>,
/** Creates a [ShareouselPreviewViewModel] for a [PreviewModel] present in [previews]. */
val preview:
(
@@ -104,10 +107,14 @@ object ShareouselViewModelModule {
selectionInteractor.aggregateContentType.zip(selectionInteractor.amountSelected) {
contentType,
numItems ->
- when (contentType) {
- ContentType.Other -> headlineGenerator.getFilesHeadline(numItems)
- ContentType.Image -> headlineGenerator.getImagesHeadline(numItems)
- ContentType.Video -> headlineGenerator.getVideosHeadline(numItems)
+ if (unselectFinalItem() && numItems == 0) {
+ headlineGenerator.getNotItemsSelectedHeadline()
+ } else {
+ when (contentType) {
+ ContentType.Other -> headlineGenerator.getFilesHeadline(numItems)
+ ContentType.Image -> headlineGenerator.getImagesHeadline(numItems)
+ ContentType.Video -> headlineGenerator.getVideosHeadline(numItems)
+ }
}
},
metadataText = chooserRequestInteractor.metadataText,
@@ -128,6 +135,7 @@ object ShareouselViewModelModule {
}
}
},
+ hasSelectedItems = selectionInteractor.selections.map { it.isNotEmpty() },
preview = { key, previewHeight, index, previewScope ->
keySet.value?.maybeLoad(index)
val previewInteractor = interactor.preview(key)