summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp2
-rw-r--r--aconfig/FeatureFlags.aconfig10
-rw-r--r--java/res/values-it/strings.xml9
-rw-r--r--java/res/values-sq/strings.xml9
-rw-r--r--java/src/com/android/intentresolver/ChooserActionFactory.java14
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java3
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java14
-rw-r--r--java/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoader.kt5
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ImageLoader.kt3
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt78
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt4
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt19
-rw-r--r--java/src/com/android/intentresolver/icons/CachingTargetDataLoader.kt36
-rw-r--r--java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt5
-rw-r--r--java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java5
-rw-r--r--java/src/com/android/intentresolver/icons/TargetDataLoader.kt4
-rw-r--r--java/src/com/android/intentresolver/widget/ViewExtensions.kt7
-rw-r--r--tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java5
-rw-r--r--tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt6
-rw-r--r--tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt4
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt49
-rw-r--r--tests/unit/src/com/android/intentresolver/icons/CachingTargetDataLoaderTest.kt117
22 files changed, 336 insertions, 72 deletions
diff --git a/Android.bp b/Android.bp
index d0d20cdb..75e29a8c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -99,7 +99,7 @@ android_app {
enabled: true,
optimize: true,
shrink: true,
- shrink_resources: true,
+ optimized_shrink_resources: true,
proguard_flags_files: ["proguard.flags"],
},
visibility: ["//visibility:public"],
diff --git a/aconfig/FeatureFlags.aconfig b/aconfig/FeatureFlags.aconfig
index a102328a..71974cf8 100644
--- a/aconfig/FeatureFlags.aconfig
+++ b/aconfig/FeatureFlags.aconfig
@@ -71,6 +71,16 @@ flag {
}
flag {
+ name: "fix_partial_image_edit_transition"
+ namespace: "intentresolver"
+ description: "Do not run the shared element transition animation for a partially visible image"
+ bug: "339583191"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "fix_private_space_locked_on_restart"
namespace: "intentresolver"
description: "Dismiss Share sheet on restart if private space became locked while stopped"
diff --git a/java/res/values-it/strings.xml b/java/res/values-it/strings.xml
index 277d03d9..75fe0b77 100644
--- a/java/res/values-it/strings.xml
+++ b/java/res/values-it/strings.xml
@@ -102,10 +102,7 @@
<string name="exclude_link" msgid="1332778255031992228">"Escludi link"</string>
<string name="include_link" msgid="827855767220339802">"Includi link"</string>
<string name="pinned" msgid="7623664001331394139">"Elemento fissato"</string>
- <!-- no translation found for selectable_image (3157858923437182271) -->
- <skip />
- <!-- no translation found for selectable_video (1271768647699300826) -->
- <skip />
- <!-- no translation found for selectable_item (7557320816744205280) -->
- <skip />
+ <string name="selectable_image" msgid="3157858923437182271">"Immagine selezionabile"</string>
+ <string name="selectable_video" msgid="1271768647699300826">"Video selezionabile"</string>
+ <string name="selectable_item" msgid="7557320816744205280">"Elemento selezionabile"</string>
</resources>
diff --git a/java/res/values-sq/strings.xml b/java/res/values-sq/strings.xml
index dc4257b2..faf27da5 100644
--- a/java/res/values-sq/strings.xml
+++ b/java/res/values-sq/strings.xml
@@ -102,10 +102,7 @@
<string name="exclude_link" msgid="1332778255031992228">"Përjashto lidhjen"</string>
<string name="include_link" msgid="827855767220339802">"Përfshi lidhjen"</string>
<string name="pinned" msgid="7623664001331394139">"U gozhdua"</string>
- <!-- no translation found for selectable_image (3157858923437182271) -->
- <skip />
- <!-- no translation found for selectable_video (1271768647699300826) -->
- <skip />
- <!-- no translation found for selectable_item (7557320816744205280) -->
- <skip />
+ <string name="selectable_image" msgid="3157858923437182271">"Imazh që mund të zgjidhet"</string>
+ <string name="selectable_video" msgid="1271768647699300826">"Video që mund të zgjidhet"</string>
+ <string name="selectable_item" msgid="7557320816744205280">"Artikull që mund të zgjidhet"</string>
</resources>
diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java
index dae1ab52..cc7091e4 100644
--- a/java/src/com/android/intentresolver/ChooserActionFactory.java
+++ b/java/src/com/android/intentresolver/ChooserActionFactory.java
@@ -16,6 +16,8 @@
package com.android.intentresolver;
+import static com.android.intentresolver.widget.ViewExtensionsKt.isFullyVisible;
+
import android.app.Activity;
import android.app.ActivityOptions;
import android.app.PendingIntent;
@@ -131,7 +133,8 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
ActionActivityStarter activityStarter,
@Nullable ShareResultSender shareResultSender,
Consumer</* @Nullable */ Integer> finishCallback,
- ClipboardManager clipboardManager) {
+ ClipboardManager clipboardManager,
+ FeatureFlags featureFlags) {
this(
context,
makeCopyButtonRunnable(
@@ -147,7 +150,8 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
imageEditor),
firstVisibleImageQuery,
activityStarter,
- log),
+ log,
+ featureFlags.fixPartialImageEditTransition()),
chooserActions,
onUpdateSharedTextIsExcluded,
log,
@@ -336,7 +340,8 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
@Nullable TargetInfo editSharingTarget,
Callable</* @Nullable */ View> firstVisibleImageQuery,
ActionActivityStarter activityStarter,
- EventLog log) {
+ EventLog log,
+ boolean requireFullVisibility) {
if (editSharingTarget == null) return null;
return () -> {
// Log share completion via edit.
@@ -347,7 +352,8 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
firstImageView = firstVisibleImageQuery.call();
} catch (Exception e) { /* ignore */ }
// Action bar is user-independent; always start as primary.
- if (firstImageView == null) {
+ if (firstImageView == null
+ || (requireFullVisibility && !isFullyVisible(firstImageView))) {
activityStarter.safelyStartActivityAsPersonalProfileUser(editSharingTarget);
} else {
activityStarter.safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 7353ff37..670512ac 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -2212,7 +2212,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
},
mShareResultSender,
this::finishWithStatus,
- mClipboardManager);
+ mClipboardManager,
+ mFeatureFlags);
}
private Supplier<ActionRow.Action> createModifyShareActionFactory() {
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java
index 8b848e55..ff0c40d7 100644
--- a/java/src/com/android/intentresolver/ChooserListAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -479,17 +479,23 @@ public class ChooserListAdapter extends ResolverListAdapter {
private void loadDirectShareIcon(SelectableTargetInfo info) {
if (mRequestedIcons.add(info)) {
- mTargetDataLoader.loadDirectShareIcon(
+ Drawable icon = mTargetDataLoader.getOrLoadDirectShareIcon(
info,
getUserHandle(),
- (drawable) -> onDirectShareIconLoaded(info, drawable));
+ (drawable) -> onDirectShareIconLoaded(info, drawable, true));
+ if (icon != null) {
+ onDirectShareIconLoaded(info, icon, false);
+ }
}
}
- private void onDirectShareIconLoaded(SelectableTargetInfo mTargetInfo, Drawable icon) {
+ private void onDirectShareIconLoaded(
+ SelectableTargetInfo mTargetInfo, @Nullable Drawable icon, boolean notify) {
if (icon != null && !mTargetInfo.hasDisplayIcon()) {
mTargetInfo.getDisplayIconHolder().setDisplayIcon(icon);
- notifyDataSetChanged();
+ if (notify) {
+ notifyDataSetChanged();
+ }
}
}
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/ShareouselComposable.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt
index 8e2626bf..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
@@ -57,6 +58,8 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.intentresolver.R
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.getOrDefault
import com.android.intentresolver.contentpreview.payloadtoggle.shared.ContentType
import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselPreviewViewModel
@@ -124,14 +127,14 @@ private fun PreviewCarousel(
}
}
- ShareouselCard(viewModel.preview(model, previewIndex))
+ ShareouselCard(viewModel.preview(model, previewIndex, rememberCoroutineScope()))
}
}
}
@Composable
private fun ShareouselCard(viewModel: ShareouselPreviewViewModel) {
- val bitmap by viewModel.bitmap.collectAsStateWithLifecycle(initialValue = null)
+ val bitmapLoadState by viewModel.bitmapLoadState.collectAsStateWithLifecycle()
val selected by viewModel.isSelected.collectAsStateWithLifecycle(initialValue = false)
val borderColor = MaterialTheme.colorScheme.primary
val scope = rememberCoroutineScope()
@@ -141,39 +144,56 @@ private fun ShareouselCard(viewModel: ShareouselPreviewViewModel) {
ContentType.Video -> stringResource(R.string.selectable_video)
else -> stringResource(R.string.selectable_item)
}
- ShareouselCard(
- image = {
- // TODO: max ratio is actually equal to the viewport ratio
- val aspectRatio = viewModel.aspectRatio.coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO)
- bitmap?.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))
- }
- },
- contentType = viewModel.contentType,
- selected = selected,
+ Crossfade(
+ targetState = bitmapLoadState,
modifier =
- Modifier.thenIf(selected) {
- 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 540229c9..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
@@ -17,13 +17,15 @@
package com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel
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 bitmap: Flow<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 c3ad7b6c..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
@@ -24,6 +24,7 @@ import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.CustomActionsInteractor
import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.SelectablePreviewsInteractor
import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.SelectionInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate
import com.android.intentresolver.contentpreview.payloadtoggle.shared.ContentType
import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
@@ -55,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
@@ -112,7 +114,7 @@ interface ShareouselViewModelModule {
}
}
},
- preview = { key, index ->
+ preview = { key, index, previewScope ->
keySet.value?.maybeLoad(index)
val previewInteractor = interactor.preview(key)
val contentType =
@@ -121,8 +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(
- bitmap = flow { emit(key.previewUri?.let { imageLoader(it) }) },
+ bitmapLoadState =
+ flow {
+ emit(
+ key.previewUri?.let { ValueUpdate.Value(imageLoader(it)) }
+ ?: ValueUpdate.Absent
+ )
+ }
+ .stateIn(previewScope, SharingStarted.Eagerly, initialBitmapValue),
contentType = contentType,
isSelected = previewInteractor.isSelected,
setSelected = previewInteractor::setSelected,
diff --git a/java/src/com/android/intentresolver/icons/CachingTargetDataLoader.kt b/java/src/com/android/intentresolver/icons/CachingTargetDataLoader.kt
index b3054231..8474b4c3 100644
--- a/java/src/com/android/intentresolver/icons/CachingTargetDataLoader.kt
+++ b/java/src/com/android/intentresolver/icons/CachingTargetDataLoader.kt
@@ -28,7 +28,7 @@ import javax.inject.Qualifier
@Qualifier @MustBeDocumented @Retention(AnnotationRetention.BINARY) annotation class Caching
-private typealias IconCache = LruCache<ComponentName, Drawable>
+private typealias IconCache = LruCache<String, Drawable>
class CachingTargetDataLoader(
private val targetDataLoader: TargetDataLoader,
@@ -49,18 +49,27 @@ class CachingTargetDataLoader(
}
}
- override fun loadDirectShareIcon(
+ override fun getOrLoadDirectShareIcon(
info: SelectableTargetInfo,
userHandle: UserHandle,
callback: Consumer<Drawable>
- ) = targetDataLoader.loadDirectShareIcon(info, userHandle, callback)
+ ): Drawable? {
+ val cacheKey = info.toCacheKey()
+ return cacheKey?.let { getCachedAppIcon(it, userHandle) }
+ ?: targetDataLoader.getOrLoadDirectShareIcon(info, userHandle) { drawable ->
+ if (cacheKey != null) {
+ getProfileIconCache(userHandle).put(cacheKey, drawable)
+ }
+ callback.accept(drawable)
+ }
+ }
override fun loadLabel(info: DisplayResolveInfo, callback: Consumer<LabelInfo>) =
targetDataLoader.loadLabel(info, callback)
override fun getOrLoadLabel(info: DisplayResolveInfo) = targetDataLoader.getOrLoadLabel(info)
- private fun getCachedAppIcon(component: ComponentName, userHandle: UserHandle): Drawable? =
+ private fun getCachedAppIcon(component: String, userHandle: UserHandle): Drawable? =
getProfileIconCache(userHandle)[component]
private fun getProfileIconCache(userHandle: UserHandle): IconCache =
@@ -70,7 +79,20 @@ class CachingTargetDataLoader(
private fun DisplayResolveInfo.toCacheKey() =
ComponentName(
- resolveInfo.activityInfo.packageName,
- resolveInfo.activityInfo.name,
- )
+ resolveInfo.activityInfo.packageName,
+ resolveInfo.activityInfo.name,
+ )
+ .flattenToString()
+
+ private fun SelectableTargetInfo.toCacheKey(): String? =
+ if (chooserTargetIcon != null) {
+ // do not cache icons for caller-provided targets
+ null
+ } else {
+ buildString {
+ append(chooserTargetComponentName?.flattenToString() ?: "")
+ append("|")
+ append(directShareShortcutInfo?.id ?: "")
+ }
+ }
}
diff --git a/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt b/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt
index 1a724d73..e7392f58 100644
--- a/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt
+++ b/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt
@@ -77,11 +77,11 @@ class DefaultTargetDataLoader(
return null
}
- override fun loadDirectShareIcon(
+ override fun getOrLoadDirectShareIcon(
info: SelectableTargetInfo,
userHandle: UserHandle,
callback: Consumer<Drawable>,
- ) {
+ ): Drawable? {
val taskId = nextTaskId.getAndIncrement()
LoadDirectShareIconTask(
context.createContextAsUser(userHandle, 0),
@@ -93,6 +93,7 @@ class DefaultTargetDataLoader(
}
.also { addTask(taskId, it) }
.executeOnExecutor(executor)
+ return null
}
override fun loadLabel(info: DisplayResolveInfo, callback: Consumer<LabelInfo>) {
diff --git a/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java b/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java
index 0f135d63..e2c0362d 100644
--- a/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java
+++ b/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java
@@ -57,7 +57,7 @@ class LoadDirectShareIconTask extends BaseLoadIconTask {
@Override
protected Drawable doInBackground(Void... voids) {
- Drawable drawable;
+ Drawable drawable = null;
Trace.beginSection("shortcut-icon");
try {
final Icon icon = mTargetInfo.getChooserTargetIcon();
@@ -70,6 +70,8 @@ class LoadDirectShareIconTask extends BaseLoadIconTask {
} else {
Log.e(TAG, "Failed to load shortcut icon for "
+ mTargetInfo.getChooserTargetComponentName() + "; no access");
+ }
+ if (drawable == null) {
drawable = loadIconPlaceholder();
}
} catch (Exception e) {
@@ -86,6 +88,7 @@ class LoadDirectShareIconTask extends BaseLoadIconTask {
}
@WorkerThread
+ @Nullable
private Drawable getChooserTargetIconDrawable(
Context context,
@Nullable Icon icon,
diff --git a/java/src/com/android/intentresolver/icons/TargetDataLoader.kt b/java/src/com/android/intentresolver/icons/TargetDataLoader.kt
index 7789df44..935b527a 100644
--- a/java/src/com/android/intentresolver/icons/TargetDataLoader.kt
+++ b/java/src/com/android/intentresolver/icons/TargetDataLoader.kt
@@ -32,11 +32,11 @@ abstract class TargetDataLoader {
): Drawable?
/** Load a shortcut icon */
- abstract fun loadDirectShareIcon(
+ abstract fun getOrLoadDirectShareIcon(
info: SelectableTargetInfo,
userHandle: UserHandle,
callback: Consumer<Drawable>,
- )
+ ): Drawable?
/** Load target label */
abstract fun loadLabel(info: DisplayResolveInfo, callback: Consumer<LabelInfo>)
diff --git a/java/src/com/android/intentresolver/widget/ViewExtensions.kt b/java/src/com/android/intentresolver/widget/ViewExtensions.kt
index d19933f5..64aa9352 100644
--- a/java/src/com/android/intentresolver/widget/ViewExtensions.kt
+++ b/java/src/com/android/intentresolver/widget/ViewExtensions.kt
@@ -16,6 +16,7 @@
package com.android.intentresolver.widget
+import android.graphics.Rect
import android.util.Log
import android.view.View
import androidx.core.view.OneShotPreDrawListener
@@ -42,3 +43,9 @@ internal suspend fun View.waitForPreDraw(): Unit = suspendCancellableCoroutine {
)
continuation.invokeOnCancellation { callback.removeListener() }
}
+
+internal fun View.isFullyVisible(): Boolean {
+ val rect = Rect()
+ val isVisible = getLocalVisibleRect(rect)
+ return isVisible && rect.width() == width && rect.height() == height
+}
diff --git a/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java b/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java
index b46d8bc3..22633085 100644
--- a/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java
+++ b/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java
@@ -180,11 +180,12 @@ public class ResolverWrapperActivity extends ResolverActivity {
}
@Override
- public void loadDirectShareIcon(
+ @Nullable
+ public Drawable getOrLoadDirectShareIcon(
@NonNull SelectableTargetInfo info,
@NonNull UserHandle userHandle,
@NonNull Consumer<Drawable> callback) {
- mTargetDataLoader.loadDirectShareIcon(info, userHandle, callback);
+ return mTargetDataLoader.getOrLoadDirectShareIcon(info, userHandle, callback);
}
@Override
diff --git a/tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt b/tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt
index 8dfbdbdd..c8e17de4 100644
--- a/tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt
+++ b/tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt
@@ -69,6 +69,8 @@ class ChooserActionFactoryTest {
latestReturn = resultCode
}
}
+ private val featureFlags =
+ FakeFeatureFlagsImpl().apply { setFlag(Flags.FLAG_FIX_PARTIAL_IMAGE_EDIT_TRANSITION, true) }
@Before
fun setup() {
@@ -119,6 +121,7 @@ class ChooserActionFactoryTest {
/* shareResultSender = */ null,
/* finishCallback = */ {},
/* clipboardManager = */ mock(),
+ /* featureFlags = */ featureFlags,
)
assertThat(testSubject.copyButtonRunnable).isNull()
}
@@ -140,6 +143,7 @@ class ChooserActionFactoryTest {
/* shareResultSender = */ null,
/* finishCallback = */ {},
/* clipboardManager = */ mock(),
+ /* featureFlags = */ featureFlags,
)
assertThat(testSubject.copyButtonRunnable).isNull()
}
@@ -162,6 +166,7 @@ class ChooserActionFactoryTest {
/* shareResultSender = */ resultSender,
/* finishCallback = */ {},
/* clipboardManager = */ mock(),
+ /* featureFlags = */ featureFlags,
)
assertThat(testSubject.copyButtonRunnable).isNotNull()
@@ -194,6 +199,7 @@ class ChooserActionFactoryTest {
/* shareResultSender = */ null,
/* finishCallback = */ resultConsumer,
/* clipboardManager = */ mock(),
+ /* featureFlags = */ featureFlags,
)
}
}
diff --git a/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt b/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt
index 5ac4f2b0..bad3b18c 100644
--- a/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt
+++ b/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt
@@ -101,7 +101,7 @@ class ChooserListAdapterTest {
val targetInfo = createSelectableTargetInfo()
testSubject.onBindView(view, targetInfo, 0)
- verify(mTargetDataLoader, times(1)).loadDirectShareIcon(any(), any(), any())
+ verify(mTargetDataLoader, times(1)).getOrLoadDirectShareIcon(any(), any(), any())
}
@Test
@@ -117,7 +117,7 @@ class ChooserListAdapterTest {
testSubject.onBindView(view, targetInfo, 0)
- verify(mTargetDataLoader, times(1)).loadDirectShareIcon(any(), any(), any())
+ verify(mTargetDataLoader, times(1)).getOrLoadDirectShareIcon(any(), any(), any())
}
@Test
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt
index ec4a9c3e..a26b4288 100644
--- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt
@@ -40,6 +40,7 @@ import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.payloadToggleImageLoader
import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.selectablePreviewsInteractor
import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.selectionInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate
import com.android.intentresolver.contentpreview.payloadtoggle.shared.ContentType
import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
@@ -185,9 +186,14 @@ class ShareouselViewModelTest {
mimeType = "video/mpeg"
),
/* index = */ 1,
+ viewModelScope,
)
- assertWithMessage("preview bitmap is null").that(previewVm.bitmap.first()).isNotNull()
+ runCurrent()
+
+ assertWithMessage("preview bitmap is null")
+ .that((previewVm.bitmapLoadState.first() as ValueUpdate.Value).value)
+ .isNotNull()
assertThat(previewVm.isSelected.first()).isFalse()
assertThat(previewVm.contentType).isEqualTo(ContentType.Video)
@@ -199,6 +205,47 @@ class ShareouselViewModelTest {
}
@Test
+ fun previews_wontLoad() =
+ runTest(targetIntentModifier = { Intent() }) {
+ cursorPreviewsRepository.previewsModel.value =
+ PreviewsModel(
+ previewModels =
+ listOf(
+ PreviewModel(
+ uri = Uri.fromParts("scheme", "ssp", "fragment"),
+ mimeType = "image/png",
+ ),
+ PreviewModel(
+ uri = Uri.fromParts("scheme1", "ssp1", "fragment1"),
+ mimeType = "video/mpeg",
+ )
+ ),
+ startIdx = 1,
+ loadMoreLeft = null,
+ loadMoreRight = null,
+ leftTriggerIndex = 0,
+ rightTriggerIndex = 1,
+ )
+ runCurrent()
+
+ val previewVm =
+ shareouselViewModel.preview.invoke(
+ PreviewModel(
+ uri = Uri.fromParts("scheme", "ssp", "fragment"),
+ mimeType = "video/mpeg"
+ ),
+ /* index = */ 1,
+ viewModelScope,
+ )
+
+ runCurrent()
+
+ assertWithMessage("preview bitmap is not null")
+ .that((previewVm.bitmapLoadState.first() as ValueUpdate.Value).value)
+ .isNull()
+ }
+
+ @Test
fun actions() {
runTest {
assertThat(shareouselViewModel.actions.first()).isEmpty()
diff --git a/tests/unit/src/com/android/intentresolver/icons/CachingTargetDataLoaderTest.kt b/tests/unit/src/com/android/intentresolver/icons/CachingTargetDataLoaderTest.kt
new file mode 100644
index 00000000..a36b512b
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/icons/CachingTargetDataLoaderTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 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.
+ */
+
+package com.android.intentresolver.icons
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.Icon
+import android.os.UserHandle
+import com.android.intentresolver.chooser.SelectableTargetInfo
+import java.util.function.Consumer
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class CachingTargetDataLoaderTest {
+ private val userHandle = UserHandle.of(1)
+
+ @Test
+ fun doNotCacheCallerProvidedShortcuts() {
+ val callerTarget =
+ SelectableTargetInfo.newSelectableTargetInfo(
+ /* sourceInfo = */ null,
+ /* backupResolveInfo = */ null,
+ /* resolvedIntent = */ Intent(),
+ /* chooserTargetComponentName =*/ ComponentName("package", "Activity"),
+ "chooserTargetUninitializedTitle",
+ /* chooserTargetIcon =*/ Icon.createWithContentUri("content://package/icon.png"),
+ /* chooserTargetIntentExtras =*/ null,
+ /* modifiedScore =*/ 1f,
+ /* shortcutInfo = */ null,
+ /* appTarget = */ null,
+ /* referrerFillInIntent = */ Intent(),
+ ) as SelectableTargetInfo
+
+ val targetDataLoader =
+ mock<TargetDataLoader> {
+ on { getOrLoadDirectShareIcon(eq(callerTarget), eq(userHandle), any()) } doReturn
+ null
+ }
+ val testSubject = CachingTargetDataLoader(targetDataLoader)
+ val callback = Consumer<Drawable> {}
+
+ testSubject.getOrLoadDirectShareIcon(callerTarget, userHandle, callback)
+ testSubject.getOrLoadDirectShareIcon(callerTarget, userHandle, callback)
+
+ verify(targetDataLoader) {
+ 2 * { getOrLoadDirectShareIcon(eq(callerTarget), eq(userHandle), any()) }
+ }
+ }
+
+ @Test
+ fun serviceShortcutsAreCached() {
+ val context =
+ mock<Context> {
+ on { userId } doReturn 1
+ on { packageName } doReturn "package"
+ }
+ val targetInfo =
+ SelectableTargetInfo.newSelectableTargetInfo(
+ /* sourceInfo = */ null,
+ /* backupResolveInfo = */ null,
+ /* resolvedIntent = */ Intent(),
+ /* chooserTargetComponentName =*/ ComponentName("package", "Activity"),
+ "chooserTargetUninitializedTitle",
+ /* chooserTargetIcon =*/ null,
+ /* chooserTargetIntentExtras =*/ null,
+ /* modifiedScore =*/ 1f,
+ /* shortcutInfo = */ ShortcutInfo.Builder(context, "1").build(),
+ /* appTarget = */ null,
+ /* referrerFillInIntent = */ Intent(),
+ ) as SelectableTargetInfo
+
+ val targetDataLoader = mock<TargetDataLoader>()
+ doAnswer {
+ val callback = it.arguments[2] as Consumer<Drawable>
+ callback.accept(BitmapDrawable(createBitmap()))
+ null
+ }
+ .whenever(targetDataLoader)
+ .getOrLoadDirectShareIcon(eq(targetInfo), eq(userHandle), any())
+ val testSubject = CachingTargetDataLoader(targetDataLoader)
+ val callback = Consumer<Drawable> {}
+
+ testSubject.getOrLoadDirectShareIcon(targetInfo, userHandle, callback)
+ testSubject.getOrLoadDirectShareIcon(targetInfo, userHandle, callback)
+
+ verify(targetDataLoader) {
+ 1 * { getOrLoadDirectShareIcon(eq(targetInfo), eq(userHandle), any()) }
+ }
+ }
+}
+
+private fun createBitmap() = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)