summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
Diffstat (limited to 'java')
-rw-r--r--java/res/layout/image_preview_loading_item.xml32
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java2
-rw-r--r--java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java27
-rw-r--r--java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt97
-rw-r--r--java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt152
-rw-r--r--java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt19
6 files changed, 273 insertions, 56 deletions
diff --git a/java/res/layout/image_preview_loading_item.xml b/java/res/layout/image_preview_loading_item.xml
new file mode 100644
index 00000000..85020e9a
--- /dev/null
+++ b/java/res/layout/image_preview_loading_item.xml
@@ -0,0 +1,32 @@
+<!--
+ ~ 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.
+ -->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="@dimen/chooser_preview_image_width"
+ android:layout_height="@dimen/chooser_preview_image_height_tall">
+
+ <ProgressBar
+ android:id="@+id/loading_indicator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:indeterminate="true"
+ android:indeterminateTint="?androidprv:attr/materialColorPrimary"
+ android:indeterminateTintMode="src_in" />
+
+</FrameLayout>
diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
index e8367c4e..18cbb8af 100644
--- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
@@ -163,10 +163,12 @@ public final class ChooserContentPreviewUi {
UnifiedContentPreviewUi unifiedContentPreviewUi = new UnifiedContentPreviewUi(
isSingleImageShare,
+ targetIntent.getType(),
actionFactory,
imageLoader,
typeClassifier,
transitionElementStatusCallback,
+ previewData.getUriCount(),
headlineGenerator);
previewData.getFileMetadataForImagePreview(mLifecycle, unifiedContentPreviewUi::setFiles);
return unifiedContentPreviewUi;
diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
index 6385f2b6..5db5020e 100644
--- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
@@ -37,11 +37,14 @@ import java.util.Objects;
class UnifiedContentPreviewUi extends ContentPreviewUi {
private final boolean mShowEditAction;
+ @Nullable
+ private final String mIntentMimeType;
private final ChooserContentPreviewUi.ActionFactory mActionFactory;
private final ImageLoader mImageLoader;
private final MimeTypeClassifier mTypeClassifier;
private final TransitionElementStatusCallback mTransitionElementStatusCallback;
private final HeadlineGenerator mHeadlineGenerator;
+ private final int mItemCount;
@Nullable
private List<FileInfo> mFiles;
@Nullable
@@ -49,16 +52,20 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
UnifiedContentPreviewUi(
boolean isSingleImage,
+ @Nullable String intentMimeType,
ChooserContentPreviewUi.ActionFactory actionFactory,
ImageLoader imageLoader,
MimeTypeClassifier typeClassifier,
TransitionElementStatusCallback transitionElementStatusCallback,
+ int itemCount,
HeadlineGenerator headlineGenerator) {
mShowEditAction = isSingleImage;
+ mIntentMimeType = intentMimeType;
mActionFactory = actionFactory;
mImageLoader = imageLoader;
mTypeClassifier = typeClassifier;
mTransitionElementStatusCallback = transitionElementStatusCallback;
+ mItemCount = itemCount;
mHeadlineGenerator = headlineGenerator;
}
@@ -96,11 +103,19 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
ScrollableImagePreviewView imagePreview =
mContentPreviewView.requireViewById(R.id.scrollable_image_preview);
+ imagePreview.setImageLoader(mImageLoader);
imagePreview.setOnNoPreviewCallback(() -> imagePreview.setVisibility(View.GONE));
imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback);
if (mFiles != null) {
updatePreviewWithFiles(mContentPreviewView, mFiles);
+ } else {
+ displayHeadline(
+ mContentPreviewView,
+ mItemCount,
+ mTypeClassifier.isImageType(mIntentMimeType),
+ mTypeClassifier.isVideoType(mIntentMimeType));
+ imagePreview.setLoading(mItemCount);
}
return mContentPreviewView;
@@ -138,14 +153,18 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
}
}
- imagePreview.setPreviews(previews, count - previews.size(), mImageLoader);
+ imagePreview.setPreviews(previews, count - previews.size());
+ displayHeadline(contentPreviewView, count, allImages, allVideos);
+ }
+ private void displayHeadline(
+ ViewGroup layout, int count, boolean allImages, boolean allVideos) {
if (allImages) {
- displayHeadline(contentPreviewView, mHeadlineGenerator.getImagesHeadline(count));
+ displayHeadline(layout, mHeadlineGenerator.getImagesHeadline(count));
} else if (allVideos) {
- displayHeadline(contentPreviewView, mHeadlineGenerator.getVideosHeadline(count));
+ displayHeadline(layout, mHeadlineGenerator.getVideosHeadline(count));
} else {
- displayHeadline(contentPreviewView, mHeadlineGenerator.getFilesHeadline(count));
+ displayHeadline(layout, mHeadlineGenerator.getFilesHeadline(count));
}
}
}
diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
index 583a2887..d9844d7b 100644
--- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
+++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
@@ -158,22 +158,28 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
return null
}
- fun setPreviews(previews: List<Preview>, otherItemCount: Int, imageLoader: CachingImageLoader) {
- previewAdapter.reset(0, imageLoader)
+ fun setImageLoader(imageLoader: CachingImageLoader) {
+ previewAdapter.imageLoader = imageLoader
+ }
+
+ fun setLoading(totalItemCount: Int) {
+ previewAdapter.reset(totalItemCount)
+ }
+
+ fun setPreviews(previews: List<Preview>, otherItemCount: Int) {
+ previewAdapter.reset(previews.size + otherItemCount)
batchLoader?.cancel()
batchLoader =
BatchPreviewLoader(
- imageLoader,
+ previewAdapter.imageLoader ?: error("Image loader is not set"),
previews,
otherItemCount,
- onReset = { totalItemCount ->
- previewAdapter.reset(totalItemCount, imageLoader)
- },
onUpdate = previewAdapter::addPreviews,
onCompletion = {
if (!previewAdapter.hasPreviews) {
onNoPreviewCallback?.run()
}
+ previewAdapter.markLoaded()
}
)
.apply {
@@ -262,10 +268,11 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
context.resources.getString(R.string.video_preview_a11y_description)
private val filePreviewDescription =
context.resources.getString(R.string.file_preview_a11y_description)
- private var imageLoader: CachingImageLoader? = null
+ var imageLoader: CachingImageLoader? = null
private var firstImagePos = -1
private var totalItemCount: Int = 0
+ private var isLoading = false
private val hasOtherItem
get() = previews.size < totalItemCount
val hasPreviews: Boolean
@@ -273,61 +280,78 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
var transitionStatusElementCallback: TransitionElementStatusCallback? = null
- fun reset(totalItemCount: Int, imageLoader: CachingImageLoader) {
- this.imageLoader = imageLoader
+ fun reset(totalItemCount: Int) {
firstImagePos = -1
previews.clear()
this.totalItemCount = maxOf(0, totalItemCount)
+ isLoading = this.totalItemCount > 0
notifyDataSetChanged()
}
+ fun markLoaded() {
+ if (!isLoading) return
+ isLoading = false
+ if (hasOtherItem) {
+ notifyItemChanged(previews.size)
+ } else {
+ notifyItemRemoved(previews.size)
+ }
+ }
+
fun addPreviews(newPreviews: Collection<Preview>) {
if (newPreviews.isEmpty()) return
val insertPos = previews.size
val hadOtherItem = hasOtherItem
+ val wasEmpty = previews.isEmpty()
previews.addAll(newPreviews)
if (firstImagePos < 0) {
val pos = newPreviews.indexOfFirst { it.type == PreviewType.Image }
if (pos >= 0) firstImagePos = insertPos + pos
}
- notifyItemRangeInserted(insertPos, newPreviews.size)
- when {
- hadOtherItem && previews.size >= totalItemCount -> {
- notifyItemRemoved(previews.size)
- }
- !hadOtherItem && previews.size < totalItemCount -> {
- notifyItemInserted(previews.size)
+ if (wasEmpty) {
+ // we don't want any item animation in that case
+ notifyDataSetChanged()
+ } else {
+ notifyItemRangeInserted(insertPos, newPreviews.size)
+ when {
+ hadOtherItem && !hasOtherItem -> {
+ notifyItemRemoved(previews.size)
+ }
+ !hadOtherItem && hasOtherItem -> {
+ notifyItemInserted(previews.size)
+ }
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, itemType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(itemType, parent, false)
- return if (itemType == R.layout.image_preview_other_item) {
- OtherItemViewHolder(view)
- } else {
- PreviewViewHolder(
- view,
- imagePreviewDescription,
- videoPreviewDescription,
- filePreviewDescription,
- )
+ return when (itemType) {
+ R.layout.image_preview_other_item -> OtherItemViewHolder(view)
+ R.layout.image_preview_loading_item -> LoadingItemViewHolder(view)
+ else ->
+ PreviewViewHolder(
+ view,
+ imagePreviewDescription,
+ videoPreviewDescription,
+ filePreviewDescription,
+ )
}
}
- override fun getItemCount(): Int = previews.size + if (hasOtherItem) 1 else 0
+ override fun getItemCount(): Int = previews.size + if (isLoading || hasOtherItem) 1 else 0
- override fun getItemViewType(position: Int): Int {
- return if (position == previews.size) {
- R.layout.image_preview_other_item
- } else {
- R.layout.image_preview_image_item
+ override fun getItemViewType(position: Int): Int =
+ when {
+ position == previews.size && isLoading -> R.layout.image_preview_loading_item
+ position == previews.size -> R.layout.image_preview_other_item
+ else -> R.layout.image_preview_image_item
}
- }
override fun onBindViewHolder(vh: ViewHolder, position: Int) {
when (vh) {
is OtherItemViewHolder -> vh.bind(totalItemCount - previews.size)
+ is LoadingItemViewHolder -> vh.bind()
is PreviewViewHolder ->
vh.bind(
previews[position],
@@ -466,6 +490,11 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
override fun unbind() = Unit
}
+ private class LoadingItemViewHolder(view: View) : ViewHolder(view) {
+ fun bind() = Unit
+ override fun unbind() = Unit
+ }
+
private class SpacingDecoration(private val innerSpacing: Int, private val outerSpacing: Int) :
ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State) {
@@ -487,7 +516,6 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
private val imageLoader: CachingImageLoader,
previews: List<Preview>,
otherItemCount: Int,
- private val onReset: (Int) -> Unit,
private val onUpdate: (List<Preview>) -> Unit,
private val onCompletion: () -> Unit,
) {
@@ -527,9 +555,6 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
}
}
.collect {
- if (blockStart == 0) {
- onReset(totalItemCount)
- }
val updates = ArrayList<Preview>(blockEnd - blockStart)
while (blockStart < blockEnd) {
if (previewWidths[blockStart] > 0) {
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt
new file mode 100644
index 00000000..08331209
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt
@@ -0,0 +1,152 @@
+/*
+ * 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.contentpreview
+
+import android.net.Uri
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.intentresolver.R.layout.chooser_grid
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class UnifiedContentPreviewUiTest {
+ private val actionFactory =
+ mock<ChooserContentPreviewUi.ActionFactory> {
+ whenever(createCustomActions()).thenReturn(emptyList())
+ }
+ private val imageLoader = mock<ImageLoader>()
+ private val headlineGenerator =
+ mock<HeadlineGenerator> {
+ whenever(getImagesHeadline(anyInt())).thenReturn("Image Headline")
+ whenever(getVideosHeadline(anyInt())).thenReturn("Video Headline")
+ whenever(getFilesHeadline(anyInt())).thenReturn("Files Headline")
+ }
+
+ private val context
+ get() = getInstrumentation().getContext()
+
+ @Test
+ fun test_displayImagesWithoutUriMetadata_showImagesHeadline() {
+ testLoadingHeadline("image/*", files = null)
+
+ verify(headlineGenerator, times(1)).getImagesHeadline(2)
+ }
+
+ @Test
+ fun test_displayVideosWithoutUriMetadata_showImagesHeadline() {
+ testLoadingHeadline("video/*", files = null)
+
+ verify(headlineGenerator, times(1)).getVideosHeadline(2)
+ }
+
+ @Test
+ fun test_displayDocumentsWithoutUriMetadata_showImagesHeadline() {
+ testLoadingHeadline("application/pdf", files = null)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ }
+
+ @Test
+ fun test_displayMixedContentWithoutUriMetadata_showImagesHeadline() {
+ testLoadingHeadline("*/*", files = null)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ }
+
+ @Test
+ fun test_displayImagesWithUriMetadataSet_showImagesHeadline() {
+ val uri = Uri.parse("content://pkg.app/image.png")
+ val files =
+ listOf(
+ FileInfo.Builder(uri).withMimeType("image/png").build(),
+ FileInfo.Builder(uri).withMimeType("image/jpeg").build(),
+ )
+ testLoadingHeadline("image/*", files)
+
+ verify(headlineGenerator, times(1)).getImagesHeadline(2)
+ }
+
+ @Test
+ fun test_displayVideosWithUriMetadataSet_showImagesHeadline() {
+ val uri = Uri.parse("content://pkg.app/image.png")
+ val files =
+ listOf(
+ FileInfo.Builder(uri).withMimeType("video/mp4").build(),
+ FileInfo.Builder(uri).withMimeType("video/mp4").build(),
+ )
+ testLoadingHeadline("video/*", files)
+
+ verify(headlineGenerator, times(1)).getVideosHeadline(2)
+ }
+
+ @Test
+ fun test_displayImagesAndVideosWithUriMetadataSet_showImagesHeadline() {
+ val uri = Uri.parse("content://pkg.app/image.png")
+ val files =
+ listOf(
+ FileInfo.Builder(uri).withMimeType("image/png").build(),
+ FileInfo.Builder(uri).withMimeType("video/mp4").build(),
+ )
+ testLoadingHeadline("*/*", files)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ }
+
+ @Test
+ fun test_displayDocumentsWithUriMetadataSet_showImagesHeadline() {
+ val uri = Uri.parse("content://pkg.app/image.png")
+ val files =
+ listOf(
+ FileInfo.Builder(uri).withMimeType("application/pdf").build(),
+ FileInfo.Builder(uri).withMimeType("application/pdf").build(),
+ )
+ testLoadingHeadline("application/pdf", files)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ }
+
+ private fun testLoadingHeadline(intentMimeType: String, files: List<FileInfo>?) {
+ val testSubject =
+ UnifiedContentPreviewUi(
+ /*isSingleImage=*/ false,
+ intentMimeType,
+ actionFactory,
+ imageLoader,
+ DefaultMimeTypeClassifier,
+ object : TransitionElementStatusCallback {
+ override fun onTransitionElementReady(name: String) = Unit
+ override fun onAllTransitionElementsReady() = Unit
+ },
+ /*itemCount=*/ 2,
+ headlineGenerator
+ )
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout = layoutInflater.inflate(chooser_grid, null, false) as ViewGroup
+
+ files?.let(testSubject::setFiles)
+ testSubject.display(context.resources, LayoutInflater.from(context), gridLayout)
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt b/java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt
index e65cba5f..a0211308 100644
--- a/java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt
+++ b/java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt
@@ -47,7 +47,6 @@ class BatchPreviewLoaderTest {
private val dispatcher = UnconfinedTestDispatcher()
private val testScope = CoroutineScope(dispatcher)
private val onCompletion = mock<() -> Unit>()
- private val onReset = mock<(Int) -> Unit>()
private val onUpdate = mock<(List<Preview>) -> Unit>()
@Before
@@ -68,19 +67,11 @@ class BatchPreviewLoaderTest {
val uriTwo = createUri(2)
imageLoader.setUriLoadingOrder(succeed(uriTwo), succeed(uriOne))
val testSubject =
- BatchPreviewLoader(
- imageLoader,
- previews(uriOne, uriTwo),
- 0,
- onReset,
- onUpdate,
- onCompletion
- )
+ BatchPreviewLoader(imageLoader, previews(uriOne, uriTwo), 0, onUpdate, onCompletion)
testSubject.loadAspectRatios(200) { _, _, _ -> 100 }
dispatcher.scheduler.advanceUntilIdle()
verify(onCompletion, times(1)).invoke()
- verify(onReset, times(1)).invoke(2)
val list = withArgCaptor { verify(onUpdate, times(1)).invoke(capture()) }.map { it.uri }
assertThat(list).containsExactly(uriOne, uriTwo).inOrder()
}
@@ -97,7 +88,6 @@ class BatchPreviewLoaderTest {
imageLoader,
previews(uriOne, uriTwo, uriThree),
0,
- onReset,
onUpdate,
onCompletion
)
@@ -105,7 +95,6 @@ class BatchPreviewLoaderTest {
dispatcher.scheduler.advanceUntilIdle()
verify(onCompletion, times(1)).invoke()
- verify(onReset, times(1)).invoke(3)
val list = withArgCaptor { verify(onUpdate, times(1)).invoke(capture()) }.map { it.uri }
assertThat(list).containsExactly(uriOne, uriThree).inOrder()
}
@@ -126,12 +115,11 @@ class BatchPreviewLoaderTest {
}
imageLoader.setUriLoadingOrder(*loadingOrder)
val testSubject =
- BatchPreviewLoader(imageLoader, previews(*uris), 0, onReset, onUpdate, onCompletion)
+ BatchPreviewLoader(imageLoader, previews(*uris), 0, onUpdate, onCompletion)
testSubject.loadAspectRatios(200) { _, _, _ -> 100 }
dispatcher.scheduler.advanceUntilIdle()
verify(onCompletion, times(1)).invoke()
- verify(onReset, times(1)).invoke(uris.size)
val list =
captureMany { verify(onUpdate, atLeast(1)).invoke(capture()) }
.fold(ArrayList<Preview>()) { acc, update -> acc.apply { addAll(update) } }
@@ -156,12 +144,11 @@ class BatchPreviewLoaderTest {
val expectedUris = Array(uris.size / 2) { createUri(it * 2 + 1) }
imageLoader.setUriLoadingOrder(*loadingOrder)
val testSubject =
- BatchPreviewLoader(imageLoader, previews(*uris), 0, onReset, onUpdate, onCompletion)
+ BatchPreviewLoader(imageLoader, previews(*uris), 0, onUpdate, onCompletion)
testSubject.loadAspectRatios(200) { _, _, _ -> 100 }
dispatcher.scheduler.advanceUntilIdle()
verify(onCompletion, times(1)).invoke()
- verify(onReset, times(1)).invoke(uris.size)
val list =
captureMany { verify(onUpdate, atLeast(1)).invoke(capture()) }
.fold(ArrayList<Preview>()) { acc, update -> acc.apply { addAll(update) } }