summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
author Andrey Epin <ayepin@google.com> 2023-01-10 23:27:17 +0000
committer Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> 2023-01-10 23:27:17 +0000
commit5c44552208f88a2e8865cf4815b24d09fc583e80 (patch)
tree1122b518170b909b0ce19845108bfa1c63f13f53 /java
parentb06872f8cc0d9053dafc9af1904069325a941a14 (diff)
parent9847a9a28af14e6494d200cd852ddce93de7848b (diff)
Merge "EnterTransitionAnimationDelegate to track animation hold timeout" into tm-qpr-dev am: 9839aa762e am: 9847a9a28a
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/modules/IntentResolver/+/20847220 Change-Id: I00efc934b1320d1c25bd20dc40ea28f04532acc0 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
Diffstat (limited to 'java')
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java33
-rw-r--r--java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java132
-rw-r--r--java/src/com/android/intentresolver/ChooserContentPreviewUi.java127
-rw-r--r--java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt35
-rw-r--r--java/src/com/android/intentresolver/ImageLoader.kt25
-rw-r--r--java/src/com/android/intentresolver/ImagePreviewImageLoader.kt45
-rw-r--r--java/src/com/android/intentresolver/widget/ImagePreviewView.kt2
-rw-r--r--java/tests/Android.bp4
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java11
-rw-r--r--java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt111
-rw-r--r--java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt33
-rw-r--r--java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt37
12 files changed, 346 insertions, 249 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 55904fc1..7da0b96b 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -55,7 +55,6 @@ import android.content.pm.ShortcutInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
-import android.graphics.Bitmap;
import android.graphics.Insets;
import android.graphics.drawable.Drawable;
import android.net.Uri;
@@ -75,7 +74,6 @@ import android.service.chooser.ChooserAction;
import android.service.chooser.ChooserTarget;
import android.text.TextUtils;
import android.util.Log;
-import android.util.Size;
import android.util.Slog;
import android.util.SparseArray;
import android.view.View;
@@ -117,7 +115,6 @@ import com.android.internal.util.FrameworkStatsLog;
import com.google.common.collect.ImmutableList;
import java.io.File;
-import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.Collator;
@@ -246,7 +243,7 @@ public class ChooserActivity extends ResolverActivity implements
private final ExecutorService mBackgroundThreadPoolExecutor = Executors.newFixedThreadPool(5);
@Nullable
- private ChooserContentPreviewCoordinator mPreviewCoordinator;
+ private ImageLoader mPreviewImageLoader;
private int mScrollStatus = SCROLL_STATUS_IDLE;
@@ -296,10 +293,7 @@ public class ChooserActivity extends ResolverActivity implements
mChooserRequest.getTargetIntentFilter()),
mChooserRequest.getTargetIntentFilter());
- mPreviewCoordinator = new ChooserContentPreviewCoordinator(
- mBackgroundThreadPoolExecutor,
- this,
- () -> mEnterTransitionAnimationDelegate.markImagePreviewReady(false));
+ mPreviewImageLoader = createPreviewImageLoader();
super.onCreate(
savedInstanceState,
@@ -712,9 +706,7 @@ public class ChooserActivity extends ResolverActivity implements
* @param parent reference to the parent container where the view should be attached to
* @return content preview view
*/
- protected ViewGroup createContentPreviewView(
- ViewGroup parent,
- ChooserContentPreviewUi.ContentPreviewCoordinator previewCoordinator) {
+ protected ViewGroup createContentPreviewView(ViewGroup parent, ImageLoader imageLoader) {
Intent targetIntent = getTargetIntent();
int previewType = ChooserContentPreviewUi.findPreferredContentPreview(
targetIntent, getContentResolver(), this::isImageType);
@@ -763,7 +755,7 @@ public class ChooserActivity extends ResolverActivity implements
? R.layout.scrollable_chooser_action_row
: R.layout.chooser_action_row,
parent,
- previewCoordinator,
+ imageLoader,
mEnterTransitionAnimationDelegate::markImagePreviewReady,
getContentResolver(),
this::isImageType);
@@ -1528,7 +1520,7 @@ public class ChooserActivity extends ResolverActivity implements
@Override
public View buildContentPreview(ViewGroup parent) {
- return createContentPreviewView(parent, mPreviewCoordinator);
+ return createContentPreviewView(parent, mPreviewImageLoader);
}
@Override
@@ -1643,17 +1635,8 @@ public class ChooserActivity extends ResolverActivity implements
}
@VisibleForTesting
- protected Bitmap loadThumbnail(Uri uri, Size size) {
- if (uri == null || size == null) {
- return null;
- }
-
- try {
- return getContentResolver().loadThumbnail(uri, size, null);
- } catch (IOException | NullPointerException | SecurityException ex) {
- getChooserActivityLogger().logContentPreviewWarning(uri);
- }
- return null;
+ protected ImageLoader createPreviewImageLoader() {
+ return new ImagePreviewImageLoader(this, getLifecycle());
}
private void handleScroll(View view, int x, int y, int oldx, int oldy) {
@@ -2010,7 +1993,7 @@ public class ChooserActivity extends ResolverActivity implements
ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
if (contentPreviewContainer.getChildCount() == 0) {
ViewGroup contentPreviewView =
- createContentPreviewView(contentPreviewContainer, mPreviewCoordinator);
+ createContentPreviewView(contentPreviewContainer, mPreviewImageLoader);
contentPreviewContainer.addView(contentPreviewView);
}
}
diff --git a/java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java b/java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java
deleted file mode 100644
index 0b8dbe35..00000000
--- a/java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2008 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;
-
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.os.Handler;
-import android.util.Size;
-
-import androidx.annotation.MainThread;
-import androidx.annotation.Nullable;
-
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
-
-import java.util.concurrent.ExecutorService;
-import java.util.function.Consumer;
-
-/**
- * Delegate to manage deferred resource loads for content preview assets, while
- * implementing Chooser's application logic for determining timeout/success/failure conditions.
- */
-public class ChooserContentPreviewCoordinator implements
- ChooserContentPreviewUi.ContentPreviewCoordinator {
- public ChooserContentPreviewCoordinator(
- ExecutorService backgroundExecutor,
- ChooserActivity chooserActivity,
- Runnable onFailCallback) {
- this.mBackgroundExecutor = MoreExecutors.listeningDecorator(backgroundExecutor);
- this.mChooserActivity = chooserActivity;
- this.mOnFailCallback = onFailCallback;
-
- this.mImageLoadTimeoutMillis =
- chooserActivity.getResources().getInteger(R.integer.config_shortAnimTime);
- }
-
- @Override
- public void loadImage(final Uri imageUri, final Consumer<Bitmap> callback) {
- final int size = mChooserActivity.getResources().getDimensionPixelSize(
- R.dimen.chooser_preview_image_max_dimen);
-
- // TODO: apparently this timeout is only used for not holding shared element transition
- // animation for too long. If so, we already have a better place for it
- // EnterTransitionAnimationDelegate.
- mHandler.postDelayed(this::onWatchdogTimeout, mImageLoadTimeoutMillis);
-
- ListenableFuture<Bitmap> bitmapFuture = mBackgroundExecutor.submit(
- () -> mChooserActivity.loadThumbnail(imageUri, new Size(size, size)));
-
- Futures.addCallback(
- bitmapFuture,
- new FutureCallback<Bitmap>() {
- @Override
- public void onSuccess(Bitmap loadedBitmap) {
- try {
- callback.accept(loadedBitmap);
- onLoadCompleted(loadedBitmap);
- } catch (Exception e) { /* unimportant */ }
- }
-
- @Override
- public void onFailure(Throwable t) {
- callback.accept(null);
- }
- },
- mHandler::post);
- }
-
- private final ChooserActivity mChooserActivity;
- private final ListeningExecutorService mBackgroundExecutor;
- private final Runnable mOnFailCallback;
- private final int mImageLoadTimeoutMillis;
-
- // TODO: this uses a `Handler` because there doesn't seem to be a straightforward way to get a
- // `ScheduledExecutorService` that posts to the UI thread unless we use Dagger. Eventually we'll
- // use Dagger and can inject this as a `@UiThread ScheduledExecutorService`.
- private final Handler mHandler = new Handler();
-
- private boolean mAtLeastOneLoaded = false;
-
- @MainThread
- private void onWatchdogTimeout() {
- if (mChooserActivity.isFinishing()) {
- return;
- }
-
- // If at least one image loads within the timeout period, allow other loads to continue.
- if (!mAtLeastOneLoaded) {
- mOnFailCallback.run();
- }
- }
-
- @MainThread
- private void onLoadCompleted(@Nullable Bitmap loadedBitmap) {
- if (mChooserActivity.isFinishing()) {
- return;
- }
-
- // TODO: the following logic can be described as "invoke the fail callback when the first
- // image loading has failed". Historically, before we had switched from a single-threaded
- // pool to a multi-threaded pool, we first loaded the transition element's image (the image
- // preview is the only case when those callbacks matter) and aborting the animation on it's
- // failure was reasonable. With the multi-thread pool, the first result may belong to any
- // image and thus we can falsely abort the animation.
- // Now, when we track the transition view state directly and after the timeout logic will
- // be moved into ChooserActivity$EnterTransitionAnimationDelegate, we can just get rid of
- // the fail callback and the following logic altogether.
- mAtLeastOneLoaded |= loadedBitmap != null;
- boolean wholeBatchFailed = !mAtLeastOneLoaded;
-
- if (wholeBatchFailed) {
- mOnFailCallback.run();
- }
- }
-}
diff --git a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
index daded28b..88f9006f 100644
--- a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
@@ -72,21 +72,6 @@ public final class ChooserContentPreviewUi {
private static final int IMAGE_FADE_IN_MILLIS = 150;
/**
- * Delegate to handle background resource loads that are dependencies of content previews.
- */
- public interface ContentPreviewCoordinator {
- /**
- * Request that an image be loaded in the background and set into a view.
- *
- * @param imageUri The {@link Uri} of the image to load.
- *
- * TODO: it looks like clients are probably capable of passing the view directly, but the
- * deferred computation here is a closer match to the legacy model for now.
- */
- void loadImage(Uri imageUri, Consumer<Bitmap> callback);
- }
-
- /**
* Delegate to build the default system action buttons to display in the preview layout, if/when
* they're determined to be appropriate for the particular preview we display.
* TODO: clarify why action buttons are part of preview logic.
@@ -184,7 +169,7 @@ public final class ChooserContentPreviewUi {
ActionFactory actionFactory,
@LayoutRes int actionRowLayout,
ViewGroup parent,
- ContentPreviewCoordinator previewCoord,
+ ImageLoader previewImageLoader,
Consumer<Boolean> onTransitionTargetReady,
ContentResolver contentResolver,
ImageMimeTypeClassifier imageClassifier) {
@@ -200,7 +185,7 @@ public final class ChooserContentPreviewUi {
createTextPreviewActions(actionFactory),
customActions),
parent,
- previewCoord,
+ previewImageLoader,
actionRowLayout);
break;
case CONTENT_PREVIEW_IMAGE:
@@ -211,7 +196,7 @@ public final class ChooserContentPreviewUi {
createImagePreviewActions(actionFactory),
customActions),
parent,
- previewCoord,
+ previewImageLoader,
onTransitionTargetReady,
contentResolver,
imageClassifier,
@@ -226,7 +211,7 @@ public final class ChooserContentPreviewUi {
createFilePreviewActions(actionFactory),
customActions),
parent,
- previewCoord,
+ previewImageLoader,
contentResolver,
actionRowLayout);
break;
@@ -268,7 +253,7 @@ public final class ChooserContentPreviewUi {
LayoutInflater layoutInflater,
List<ActionRow.Action> actions,
ViewGroup parent,
- ContentPreviewCoordinator previewCoord,
+ ImageLoader previewImageLoader,
@LayoutRes int actionRowLayout) {
ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_text, parent, false);
@@ -313,7 +298,7 @@ public final class ChooserContentPreviewUi {
if (previewThumbnail == null) {
previewThumbnailView.setVisibility(View.GONE);
} else {
- previewCoord.loadImage(
+ previewImageLoader.loadImage(
previewThumbnail,
(bitmap) -> updateViewWithImage(
contentPreviewLayout.findViewById(
@@ -340,7 +325,7 @@ public final class ChooserContentPreviewUi {
LayoutInflater layoutInflater,
List<ActionRow.Action> actions,
ViewGroup parent,
- ContentPreviewCoordinator previewCoord,
+ ImageLoader imageLoader,
Consumer<Boolean> onTransitionTargetReady,
ContentResolver contentResolver,
ImageMimeTypeClassifier imageClassifier,
@@ -355,7 +340,6 @@ public final class ChooserContentPreviewUi {
actionRow.setActions(actions);
}
- final ImagePreviewImageLoader imageLoader = new ImagePreviewImageLoader(previewCoord);
final ArrayList<Uri> imageUris = new ArrayList<>();
String action = targetIntent.getAction();
if (Intent.ACTION_SEND.equals(action)) {
@@ -408,59 +392,74 @@ public final class ChooserContentPreviewUi {
LayoutInflater layoutInflater,
List<ActionRow.Action> actions,
ViewGroup parent,
- ContentPreviewCoordinator previewCoord,
+ ImageLoader imageLoader,
ContentResolver contentResolver,
@LayoutRes int actionRowLayout) {
ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_file, parent, false);
+ List<Uri> uris = extractFileUris(targetIntent);
+ final int uriCount = uris.size();
+
+ if (uriCount == 0) {
+ contentPreviewLayout.setVisibility(View.GONE);
+ Log.i(TAG,
+ "Appears to be no uris available in EXTRA_STREAM, removing "
+ + "preview area");
+ return contentPreviewLayout;
+ }
+
+ if (uriCount == 1) {
+ loadFileUriIntoView(uris.get(0), contentPreviewLayout, imageLoader, contentResolver);
+ } else {
+ FileInfo fileInfo = extractFileInfo(uris.get(0), contentResolver);
+ int remUriCount = uriCount - 1;
+ Map<String, Object> arguments = new HashMap<>();
+ arguments.put(PLURALS_COUNT, remUriCount);
+ arguments.put(PLURALS_FILE_NAME, fileInfo.name);
+ String fileName =
+ PluralsMessageFormatter.format(resources, arguments, R.string.file_count);
+
+ TextView fileNameView = contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_filename);
+ fileNameView.setText(fileName);
+
+ View thumbnailView = contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_file_thumbnail);
+ thumbnailView.setVisibility(View.GONE);
+
+ ImageView fileIconView = contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_file_icon);
+ fileIconView.setVisibility(View.VISIBLE);
+ fileIconView.setImageResource(R.drawable.ic_file_copy);
+ }
+
final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
if (actionRow != null) {
actionRow.setActions(actions);
}
- String action = targetIntent.getAction();
- if (Intent.ACTION_SEND.equals(action)) {
+ return contentPreviewLayout;
+ }
+
+ private static List<Uri> extractFileUris(Intent targetIntent) {
+ List<Uri> uris = new ArrayList<>();
+ if (Intent.ACTION_SEND.equals(targetIntent.getAction())) {
Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
- loadFileUriIntoView(uri, contentPreviewLayout, previewCoord, contentResolver);
+ if (uri != null) {
+ uris.add(uri);
+ }
} else {
- List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
- int uriCount = uris.size();
-
- if (uriCount == 0) {
- contentPreviewLayout.setVisibility(View.GONE);
- Log.i(TAG,
- "Appears to be no uris available in EXTRA_STREAM, removing "
- + "preview area");
- return contentPreviewLayout;
- } else if (uriCount == 1) {
- loadFileUriIntoView(
- uris.get(0), contentPreviewLayout, previewCoord, contentResolver);
- } else {
- FileInfo fileInfo = extractFileInfo(uris.get(0), contentResolver);
- int remUriCount = uriCount - 1;
- Map<String, Object> arguments = new HashMap<>();
- arguments.put(PLURALS_COUNT, remUriCount);
- arguments.put(PLURALS_FILE_NAME, fileInfo.name);
- String fileName =
- PluralsMessageFormatter.format(resources, arguments, R.string.file_count);
-
- TextView fileNameView = contentPreviewLayout.findViewById(
- com.android.internal.R.id.content_preview_filename);
- fileNameView.setText(fileName);
-
- View thumbnailView = contentPreviewLayout.findViewById(
- com.android.internal.R.id.content_preview_file_thumbnail);
- thumbnailView.setVisibility(View.GONE);
-
- ImageView fileIconView = contentPreviewLayout.findViewById(
- com.android.internal.R.id.content_preview_file_icon);
- fileIconView.setVisibility(View.VISIBLE);
- fileIconView.setImageResource(R.drawable.ic_file_copy);
+ List<Uri> receivedUris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ if (receivedUris != null) {
+ for (Uri uri : receivedUris) {
+ if (uri != null) {
+ uris.add(uri);
+ }
+ }
}
}
-
- return contentPreviewLayout;
+ return uris;
}
private static List<ActionRow.Action> createFilePreviewActions(ActionFactory actionFactory) {
@@ -494,7 +493,7 @@ public final class ChooserContentPreviewUi {
private static void loadFileUriIntoView(
final Uri uri,
final View parent,
- final ContentPreviewCoordinator previewCoord,
+ final ImageLoader imageLoader,
final ContentResolver contentResolver) {
FileInfo fileInfo = extractFileInfo(uri, contentResolver);
@@ -503,7 +502,7 @@ public final class ChooserContentPreviewUi {
fileNameView.setText(fileInfo.name);
if (fileInfo.hasThumbnail) {
- previewCoord.loadImage(
+ imageLoader.loadImage(
uri,
(bitmap) -> updateViewWithImage(
parent.findViewById(
diff --git a/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt b/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt
index a0bf61b6..31aeea44 100644
--- a/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt
+++ b/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt
@@ -15,23 +15,30 @@
*/
package com.android.intentresolver
-import android.app.Activity
import android.app.SharedElementCallback
import android.view.View
-import com.android.intentresolver.widget.ResolverDrawerLayout
+import androidx.activity.ComponentActivity
+import androidx.lifecycle.lifecycleScope
+import com.android.internal.annotations.VisibleForTesting
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import java.util.function.Supplier
/**
* A helper class to track app's readiness for the scene transition animation.
* The app is ready when both the image is laid out and the drawer offset is calculated.
*/
-internal class EnterTransitionAnimationDelegate(
- private val activity: Activity,
- private val resolverDrawerLayoutSupplier: Supplier<ResolverDrawerLayout?>
+@VisibleForTesting
+class EnterTransitionAnimationDelegate(
+ private val activity: ComponentActivity,
+ private val transitionTargetSupplier: Supplier<View?>,
) : View.OnLayoutChangeListener {
+
private var removeSharedElements = false
private var previewReady = false
private var offsetCalculated = false
+ private var timeoutJob: Job? = null
init {
activity.setEnterSharedElementCallback(
@@ -46,9 +53,23 @@ internal class EnterTransitionAnimationDelegate(
})
}
- fun postponeTransition() = activity.postponeEnterTransition()
+ fun postponeTransition() {
+ activity.postponeEnterTransition()
+ timeoutJob = activity.lifecycleScope.launch {
+ delay(activity.resources.getInteger(R.integer.config_shortAnimTime).toLong())
+ onTimeout()
+ }
+ }
+
+ private fun onTimeout() {
+ // We only mark the preview readiness and not the offset readiness
+ // (see [#markOffsetCalculated()]) as this is what legacy logic, effectively, did. We might
+ // want to review that aspect separately.
+ markImagePreviewReady(runTransitionAnimation = false)
+ }
fun markImagePreviewReady(runTransitionAnimation: Boolean) {
+ timeoutJob?.cancel()
if (!runTransitionAnimation) {
removeSharedElements = true
}
@@ -77,7 +98,7 @@ internal class EnterTransitionAnimationDelegate(
}
private fun maybeStartListenForLayout() {
- val drawer = resolverDrawerLayoutSupplier.get()
+ val drawer = transitionTargetSupplier.get()
if (previewReady && offsetCalculated && drawer != null) {
if (drawer.isInLayout) {
startPostponedEnterTransition()
diff --git a/java/src/com/android/intentresolver/ImageLoader.kt b/java/src/com/android/intentresolver/ImageLoader.kt
new file mode 100644
index 00000000..13b1dd9c
--- /dev/null
+++ b/java/src/com/android/intentresolver/ImageLoader.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 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
+
+import android.graphics.Bitmap
+import android.net.Uri
+import java.util.function.Consumer
+
+interface ImageLoader : suspend (Uri) -> Bitmap? {
+ fun loadImage(uri: Uri, callback: Consumer<Bitmap?>)
+}
diff --git a/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt
index e68eb66a..40081c87 100644
--- a/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt
+++ b/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt
@@ -16,23 +16,42 @@
package com.android.intentresolver
+import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
-import kotlinx.coroutines.suspendCancellableCoroutine
+import android.util.Size
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.coroutineScope
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.util.function.Consumer
-// TODO: convert ChooserContentPreviewCoordinator to Kotlin and merge this class into it.
-internal class ImagePreviewImageLoader(
- private val previewCoordinator: ChooserContentPreviewUi.ContentPreviewCoordinator
-) : suspend (Uri) -> Bitmap? {
+internal class ImagePreviewImageLoader @JvmOverloads constructor(
+ private val context: Context,
+ private val lifecycle: Lifecycle,
+ private val dispatcher: CoroutineDispatcher = Dispatchers.IO
+) : ImageLoader {
- override suspend fun invoke(uri: Uri): Bitmap? =
- suspendCancellableCoroutine { continuation ->
- val callback = java.util.function.Consumer<Bitmap?> { bitmap ->
- try {
- continuation.resumeWith(Result.success(bitmap))
- } catch (ignored: Exception) {
- }
+ override suspend fun invoke(uri: Uri): Bitmap? = loadImageAsync(uri)
+
+ override fun loadImage(uri: Uri, callback: Consumer<Bitmap?>) {
+ lifecycle.coroutineScope.launch {
+ val image = loadImageAsync(uri)
+ if (isActive) {
+ callback.accept(image)
}
- previewCoordinator.loadImage(uri, callback)
}
+ }
+
+ private suspend fun loadImageAsync(uri: Uri): Bitmap? {
+ val size = context.resources.getDimensionPixelSize(R.dimen.chooser_preview_image_max_dimen)
+ return withContext(dispatcher) {
+ runCatching {
+ context.contentResolver.loadThumbnail(uri, Size(size, size), null)
+ }.getOrNull()
+ }
+ }
}
diff --git a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt
index a37ef954..c61c7c72 100644
--- a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt
+++ b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt
@@ -35,7 +35,7 @@ import kotlinx.coroutines.launch
import java.util.function.Consumer
import com.android.internal.R as IntR
-typealias ImageLoader = suspend (Uri) -> Bitmap?
+private typealias ImageLoader = suspend (Uri) -> Bitmap?
private const val IMAGE_FADE_IN_MILLIS = 150L
diff --git a/java/tests/Android.bp b/java/tests/Android.bp
index ee63e56d..cf149353 100644
--- a/java/tests/Android.bp
+++ b/java/tests/Android.bp
@@ -23,9 +23,13 @@ android_test {
"androidx.test.ext.junit",
"mockito-target-minus-junit4",
"androidx.test.espresso.core",
+ "androidx.lifecycle_lifecycle-common-java8",
+ "androidx.lifecycle_lifecycle-extensions",
+ "androidx.lifecycle_lifecycle-runtime-ktx",
"truth-prebuilt",
"testables",
"testng",
+ "kotlinx_coroutines_test",
],
test_suites: ["general-tests"],
sdk_version: "core_platform",
diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
index 97de97f5..6bc5e12a 100644
--- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
+++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
@@ -30,10 +30,8 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.database.Cursor;
-import android.graphics.Bitmap;
import android.net.Uri;
import android.os.UserHandle;
-import android.util.Size;
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider;
@@ -208,11 +206,10 @@ public class ChooserWrapperActivity
}
@Override
- protected Bitmap loadThumbnail(Uri uri, Size size) {
- if (sOverrides.previewThumbnail != null) {
- return sOverrides.previewThumbnail;
- }
- return super.loadThumbnail(uri, size);
+ protected ImageLoader createPreviewImageLoader() {
+ return new TestPreviewImageLoader(
+ super.createPreviewImageLoader(),
+ () -> sOverrides.previewThumbnail);
}
@Override
diff --git a/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt b/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt
new file mode 100644
index 00000000..ffe89400
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2023 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
+
+import android.content.res.Resources
+import android.view.View
+import android.view.Window
+import androidx.activity.ComponentActivity
+import androidx.lifecycle.Lifecycle
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+private const val TIMEOUT_MS = 200
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class EnterTransitionAnimationDelegateTest {
+ private val scheduler = TestCoroutineScheduler()
+ private val dispatcher = StandardTestDispatcher(scheduler)
+ private val lifecycleOwner = TestLifecycleOwner()
+
+ private val transitionTargetView = mock<View> {
+ // avoid the request-layout path in the delegate
+ whenever(isInLayout).thenReturn(true)
+ }
+
+ private val windowMock = mock<Window>()
+ private val resourcesMock = mock<Resources> {
+ whenever(getInteger(anyInt())).thenReturn(TIMEOUT_MS)
+ }
+ private val activity = mock<ComponentActivity> {
+ whenever(lifecycle).thenReturn(lifecycleOwner.lifecycle)
+ whenever(resources).thenReturn(resourcesMock)
+ whenever(isActivityTransitionRunning).thenReturn(true)
+ whenever(window).thenReturn(windowMock)
+ }
+
+ private val testSubject = EnterTransitionAnimationDelegate(activity) {
+ transitionTargetView
+ }
+
+ @Before
+ fun setup() {
+ Dispatchers.setMain(dispatcher)
+ lifecycleOwner.state = Lifecycle.State.CREATED
+ }
+
+ @After
+ fun cleanup() {
+ lifecycleOwner.state = Lifecycle.State.DESTROYED
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun test_postponeTransition_timeout() {
+ testSubject.postponeTransition()
+ testSubject.markOffsetCalculated()
+
+ scheduler.advanceTimeBy(TIMEOUT_MS + 1L)
+ verify(activity, times(1)).startPostponedEnterTransition()
+ verify(windowMock, never()).setWindowAnimations(anyInt())
+ }
+
+ @Test
+ fun test_postponeTransition_animation_resumes_only_once() {
+ testSubject.postponeTransition()
+ testSubject.markOffsetCalculated()
+ testSubject.markImagePreviewReady(true)
+ testSubject.markOffsetCalculated()
+ testSubject.markImagePreviewReady(true)
+
+ scheduler.advanceTimeBy(TIMEOUT_MS + 1L)
+ verify(activity, times(1)).startPostponedEnterTransition()
+ }
+
+ @Test
+ fun test_postponeTransition_resume_animation_conditions() {
+ testSubject.postponeTransition()
+ verify(activity, never()).startPostponedEnterTransition()
+
+ testSubject.markOffsetCalculated()
+ verify(activity, never()).startPostponedEnterTransition()
+
+ testSubject.markImagePreviewReady(true)
+ verify(activity, times(1)).startPostponedEnterTransition()
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt b/java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt
new file mode 100644
index 00000000..f47e343f
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 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
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+
+internal class TestLifecycleOwner : LifecycleOwner {
+ private val lifecycleRegistry = LifecycleRegistry.createUnsafe(this)
+
+ override fun getLifecycle(): Lifecycle = lifecycleRegistry
+
+ var state: Lifecycle.State
+ get() = lifecycle.currentState
+ set(value) {
+ lifecycleRegistry.currentState = value
+ }
+} \ No newline at end of file
diff --git a/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt b/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt
new file mode 100644
index 00000000..fd617fdd
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 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
+
+import android.graphics.Bitmap
+import android.net.Uri
+import java.util.function.Consumer
+
+internal class TestPreviewImageLoader(
+ private val imageLoader: ImageLoader,
+ private val imageOverride: () -> Bitmap?
+) : ImageLoader {
+ override fun loadImage(uri: Uri, callback: Consumer<Bitmap?>) {
+ val override = imageOverride()
+ if (override != null) {
+ callback.accept(override)
+ } else {
+ imageLoader.loadImage(uri, callback)
+ }
+ }
+
+ override suspend fun invoke(uri: Uri): Bitmap? = imageOverride() ?: imageLoader(uri)
+}