summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
author Andrey Epin <ayepin@google.com> 2023-09-20 15:02:21 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2023-09-20 15:02:21 +0000
commit07c906b2c45e406effb57cc70c51e4fa12dc4c59 (patch)
tree3a9c196a46423d815a52cb19f0655cbb580252c5 /java
parentc7bcd3858d5aa3b7750890abfec73f40e175357b (diff)
parent5e4370b5de1347909d514e279e1aada582458820 (diff)
Merge "Make preview scrollable under a feature flag." into main
Diffstat (limited to 'java')
-rw-r--r--java/res/layout/chooser_grid_scrollable_preview.xml128
-rw-r--r--java/res/layout/chooser_list_per_profile_wrap.xml42
-rw-r--r--java/res/values/attrs.xml5
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java29
-rw-r--r--java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java25
-rw-r--r--java/src/com/android/intentresolver/grid/ChooserGridAdapter.java34
-rw-r--r--java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt90
-rw-r--r--java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java96
-rw-r--r--java/tests/Android.bp2
-rw-r--r--java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java51
-rw-r--r--java/tests/src/com/android/intentresolver/contentpreview/FileContentPreviewUiTest.kt45
-rw-r--r--java/tests/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt199
-rw-r--r--java/tests/src/com/android/intentresolver/contentpreview/TextContentPreviewUiTest.kt44
-rw-r--r--java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt235
14 files changed, 947 insertions, 78 deletions
diff --git a/java/res/layout/chooser_grid_scrollable_preview.xml b/java/res/layout/chooser_grid_scrollable_preview.xml
new file mode 100644
index 00000000..a5ac75a2
--- /dev/null
+++ b/java/res/layout/chooser_grid_scrollable_preview.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright 2015, 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.
+*/
+-->
+<com.android.intentresolver.widget.ResolverDrawerLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ app:maxCollapsedHeight="0dp"
+ app:maxCollapsedHeightSmall="56dp"
+ app:useScrollablePreviewNestedFlingLogic="true"
+ android:maxWidth="@dimen/chooser_width"
+ android:id="@androidprv:id/contentPanel">
+
+ <RelativeLayout
+ android:id="@androidprv:id/chooser_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_alwaysShow="true"
+ android:elevation="0dp"
+ android:background="@drawable/bottomsheet_background">
+
+ <View
+ android:id="@androidprv:id/drag"
+ android:layout_width="64dp"
+ android:layout_height="4dp"
+ android:background="@drawable/ic_drag_handle"
+ android:layout_marginTop="@dimen/chooser_edge_margin_thin"
+ android:layout_marginBottom="@dimen/chooser_edge_margin_thin"
+ android:layout_centerHorizontal="true"
+ android:layout_alignParentTop="true" />
+
+ <TextView android:id="@android:id/title"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:textAppearance="@android:style/TextAppearance.DeviceDefault.WindowTitle"
+ android:gravity="center"
+ android:paddingBottom="@dimen/chooser_view_spacing"
+ android:paddingLeft="24dp"
+ android:paddingRight="24dp"
+ android:visibility="gone"
+ android:layout_below="@androidprv:id/drag"
+ android:layout_centerHorizontal="true"/>
+ </RelativeLayout>
+
+ <FrameLayout
+ android:id="@+id/chooser_headline_row_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_alwaysShow="true"
+ android:background="?androidprv:attr/materialColorSurfaceContainer">
+
+ <ViewStub
+ android:id="@+id/chooser_headline_row_stub"
+ android:inflatedId="@+id/chooser_headline_row"
+ android:layout="@layout/chooser_headline_row"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="@dimen/chooser_edge_margin_normal"
+ android:layout_marginBottom="@dimen/chooser_view_spacing" />
+ </FrameLayout>
+
+ <com.android.intentresolver.widget.ChooserNestedScrollView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:background="#f00">
+
+ <FrameLayout
+ android:id="@androidprv:id/content_preview_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone" />
+
+ <TabHost
+ android:id="@androidprv:id/profile_tabhost"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:background="?androidprv:attr/materialColorSurfaceContainer">
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <TabWidget
+ android:id="@android:id/tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone">
+ </TabWidget>
+ <FrameLayout
+ android:id="@android:id/tabcontent"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <com.android.intentresolver.ResolverViewPager
+ android:id="@androidprv:id/profile_pager"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+ </FrameLayout>
+ </LinearLayout>
+ </TabHost>
+ </LinearLayout>
+
+ </com.android.intentresolver.widget.ChooserNestedScrollView>
+
+</com.android.intentresolver.widget.ResolverDrawerLayout>
diff --git a/java/res/layout/chooser_list_per_profile_wrap.xml b/java/res/layout/chooser_list_per_profile_wrap.xml
new file mode 100644
index 00000000..157fa75d
--- /dev/null
+++ b/java/res/layout/chooser_list_per_profile_wrap.xml
@@ -0,0 +1,42 @@
+<!--
+ ~ Copyright (C) 2019 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.
+ -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:descendantFocusability="blocksDescendants">
+ <!-- ^^^ Block descendants from receiving focus to prevent NestedScrollView
+ (ChooserNestedScrollView) scrolling to the focused view when switching tabs. Without it, TabHost
+ view will request focus on the newly activated tab. The RecyclerView from this layout gets
+ focused and notifies its parents (including NestedScrollView) about it through
+ #requestChildFocus method call. NestedScrollView's view implementation of the method will
+ scroll to the focused view. -->
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layoutManager="com.android.intentresolver.ChooserGridLayoutManager"
+ android:id="@androidprv:id/resolver_list"
+ android:clipToPadding="false"
+ android:background="?androidprv:attr/materialColorSurfaceContainer"
+ android:scrollbars="none"
+ android:elevation="1dp"
+ android:nestedScrollingEnabled="true" />
+
+ <include layout="@layout/resolver_empty_states" />
+</RelativeLayout>
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index 67acb3ae..c9f2c300 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -32,6 +32,11 @@
will push all ignoreOffset siblings below it when the drawer is moved i.e. setting the
top limit the ignoreOffset elements. -->
<attr name="ignoreOffsetTopLimit" format="reference" />
+ <!-- Specifies whether ResolverDrawerLayout should use an alternative nested fling logic
+ adjusted for the scrollable preview feature.
+ Controlled by the flag com.android.intentresolver.Flags#FLAG_SCROLLABLE_PREVIEW.
+ -->
+ <attr name="useScrollablePreviewNestedFlingLogic" format="boolean" />
</declare-styleable>
<declare-styleable name="ResolverDrawerLayout_LayoutParams">
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 26dbd224..f455be4c 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -491,7 +491,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
/* workProfileQuietModeChecker= */ () -> false,
/* workProfileUserHandle= */ null,
getAnnotatedUserHandles().cloneProfileUserHandle,
- mMaxTargetsPerRow);
+ mMaxTargetsPerRow,
+ mFeatureFlags);
}
private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles(
@@ -525,7 +526,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
selectedProfile,
getAnnotatedUserHandles().workProfileUserHandle,
getAnnotatedUserHandles().cloneProfileUserHandle,
- mMaxTargetsPerRow);
+ mMaxTargetsPerRow,
+ mFeatureFlags);
}
private int findSelectedProfile() {
@@ -667,7 +669,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
getResources(),
getLayoutInflater(),
parent,
- /*headlineViewParent=*/null);
+ mFeatureFlags.scrollablePreview()
+ ? findViewById(R.id.chooser_headline_row_container)
+ : null);
if (layout != null) {
adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
@@ -788,7 +792,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
@Override
public int getLayoutResource() {
- return R.layout.chooser_grid;
+ return mFeatureFlags.scrollablePreview()
+ ? R.layout.chooser_grid_scrollable_preview
+ : R.layout.chooser_grid;
}
@Override // ResolverListCommunicator
@@ -1208,7 +1214,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
},
chooserListAdapter,
shouldShowContentPreview(),
- mMaxTargetsPerRow);
+ mMaxTargetsPerRow,
+ mFeatureFlags);
}
@VisibleForTesting
@@ -1639,11 +1646,13 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
}
private boolean shouldShowStickyContentPreviewNoOrientationCheck() {
- return shouldShowTabs()
- && (mMultiProfilePagerAdapter.getListAdapterForUserHandle(
- UserHandle.of(UserHandle.myUserId())).getCount() > 0
- || shouldShowContentPreviewWhenEmpty())
- && shouldShowContentPreview();
+ if (!shouldShowContentPreview()) {
+ return false;
+ }
+ boolean isEmpty = mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+ UserHandle.of(UserHandle.myUserId())).getCount() == 0;
+ return (mFeatureFlags.scrollablePreview() || shouldShowTabs())
+ && (!isEmpty || shouldShowContentPreviewWhenEmpty());
}
/**
diff --git a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
index c159243e..ba35ae5d 100644
--- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
@@ -52,7 +52,8 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
Supplier<Boolean> workProfileQuietModeChecker,
UserHandle workProfileUserHandle,
UserHandle cloneProfileUserHandle,
- int maxTargetsPerRow) {
+ int maxTargetsPerRow,
+ FeatureFlags featureFlags) {
this(
context,
new ChooserProfileAdapterBinder(maxTargetsPerRow),
@@ -62,7 +63,8 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
/* defaultProfile= */ 0,
workProfileUserHandle,
cloneProfileUserHandle,
- new BottomPaddingOverrideSupplier(context));
+ new BottomPaddingOverrideSupplier(context),
+ featureFlags);
}
ChooserMultiProfilePagerAdapter(
@@ -74,7 +76,8 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
@Profile int defaultProfile,
UserHandle workProfileUserHandle,
UserHandle cloneProfileUserHandle,
- int maxTargetsPerRow) {
+ int maxTargetsPerRow,
+ FeatureFlags featureFlags) {
this(
context,
new ChooserProfileAdapterBinder(maxTargetsPerRow),
@@ -84,7 +87,8 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
defaultProfile,
workProfileUserHandle,
cloneProfileUserHandle,
- new BottomPaddingOverrideSupplier(context));
+ new BottomPaddingOverrideSupplier(context),
+ featureFlags);
}
private ChooserMultiProfilePagerAdapter(
@@ -96,7 +100,8 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
@Profile int defaultProfile,
UserHandle workProfileUserHandle,
UserHandle cloneProfileUserHandle,
- BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {
+ BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier,
+ FeatureFlags featureFlags) {
super(
context,
gridAdapter -> gridAdapter.getListAdapter(),
@@ -107,7 +112,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
defaultProfile,
workProfileUserHandle,
cloneProfileUserHandle,
- () -> makeProfileView(context),
+ () -> makeProfileView(context, featureFlags),
bottomPaddingOverrideSupplier);
mAdapterBinder = adapterBinder;
mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier;
@@ -131,10 +136,12 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
}
}
- private static ViewGroup makeProfileView(Context context) {
+ private static ViewGroup makeProfileView(
+ Context context, FeatureFlags featureFlags) {
LayoutInflater inflater = LayoutInflater.from(context);
- ViewGroup rootView = (ViewGroup) inflater.inflate(
- R.layout.chooser_list_per_profile, null, false);
+ ViewGroup rootView = featureFlags.scrollablePreview()
+ ? (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile_wrap, null, false)
+ : (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile, null, false);
RecyclerView recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list);
recyclerView.setAccessibilityDelegateCompat(
new ChooserRecyclerViewAccessibilityDelegate(recyclerView));
diff --git a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
index fadea934..2ab38f30 100644
--- a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
+++ b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
@@ -32,9 +32,12 @@ import android.view.animation.DecelerateInterpolator;
import android.widget.Space;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.FeatureFlags;
import com.android.intentresolver.R;
import com.android.intentresolver.ResolverListAdapter.ViewHolder;
import com.android.internal.annotations.VisibleForTesting;
@@ -107,6 +110,9 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
private final boolean mShouldShowContentPreview;
private final int mChooserWidthPixels;
private final int mChooserRowTextOptionTranslatePixelSize;
+ private final FeatureFlags mFeatureFlags;
+ @Nullable
+ private RecyclerView mRecyclerView;
private int mChooserTargetWidth = 0;
@@ -119,7 +125,8 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
ChooserActivityDelegate chooserActivityDelegate,
ChooserListAdapter wrappedAdapter,
boolean shouldShowContentPreview,
- int maxTargetsPerRow) {
+ int maxTargetsPerRow,
+ FeatureFlags featureFlags) {
super();
mChooserActivityDelegate = chooserActivityDelegate;
@@ -133,6 +140,7 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
mChooserWidthPixels = context.getResources().getDimensionPixelSize(R.dimen.chooser_width);
mChooserRowTextOptionTranslatePixelSize = context.getResources().getDimensionPixelSize(
R.dimen.chooser_row_text_option_translate);
+ mFeatureFlags = featureFlags;
wrappedAdapter.registerDataSetObserver(new DataSetObserver() {
@Override
@@ -149,6 +157,18 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
});
}
+ @Override
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+ if (mFeatureFlags.scrollablePreview()) {
+ mRecyclerView = recyclerView;
+ }
+ }
+
+ @Override
+ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+ mRecyclerView = null;
+ }
+
public void setFooterHeight(int height) {
mFooterHeight = height;
}
@@ -198,7 +218,8 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
public int getSystemRowCount() {
// For the tabbed case we show the sticky content preview above the tabs,
// please refer to shouldShowStickyContentPreview
- if (mChooserActivityDelegate.shouldShowTabs()) {
+ if (mChooserActivityDelegate.shouldShowTabs()
+ || mFeatureFlags.scrollablePreview()) {
return 0;
}
@@ -318,6 +339,15 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
mAzLabelVisibility = isVisible;
int azRowPos = getAzLabelRowPosition();
if (azRowPos >= 0) {
+ if (mRecyclerView != null) {
+ for (int i = 0, size = mRecyclerView.getChildCount(); i < size; i++) {
+ View child = mRecyclerView.getChildAt(i);
+ if (mRecyclerView.getChildAdapterPosition(child) == azRowPos) {
+ child.setVisibility(isVisible ? View.VISIBLE : View.GONE);
+ }
+ }
+ return;
+ }
notifyItemChanged(azRowPos);
}
}
diff --git a/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt b/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt
new file mode 100644
index 00000000..26464ca1
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt
@@ -0,0 +1,90 @@
+package com.android.intentresolver.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.LinearLayout
+import androidx.core.view.ScrollingView
+import androidx.core.view.marginBottom
+import androidx.core.view.marginLeft
+import androidx.core.view.marginRight
+import androidx.core.view.marginTop
+import androidx.core.widget.NestedScrollView
+
+/**
+ * A narrowly tailored [NestedScrollView] to be used inside [ResolverDrawerLayout] and help to
+ * orchestrate content preview scrolling. It expects one [LinearLayout] child with
+ * [LinearLayout.VERTICAL] orientation. If the child has more than one child, the first its child
+ * will be made scrollable (it is expected to be a content preview view).
+ */
+class ChooserNestedScrollView : NestedScrollView {
+ constructor(context: Context) : super(context)
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int
+ ) : super(context, attrs, defStyleAttr)
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val content =
+ getChildAt(0) as? LinearLayout ?: error("Exactly one child, LinerLayout, is expected")
+ require(content.orientation == LinearLayout.VERTICAL) { "VERTICAL orientation is expected" }
+ require(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
+ "Expected to have an exact width"
+ }
+
+ val lp = content.layoutParams ?: error("LayoutParams is missing")
+ val contentWidthSpec =
+ getChildMeasureSpec(
+ widthMeasureSpec,
+ paddingLeft + content.marginLeft + content.marginRight + paddingRight,
+ lp.width
+ )
+ val contentHeightSpec =
+ getChildMeasureSpec(
+ heightMeasureSpec,
+ paddingTop + content.marginTop + content.marginBottom + paddingBottom,
+ lp.height
+ )
+ content.measure(contentWidthSpec, contentHeightSpec)
+
+ if (content.childCount > 1) {
+ // We expect that the first child should be scrollable up
+ val child = content.getChildAt(0)
+ val height =
+ MeasureSpec.getSize(heightMeasureSpec) +
+ child.measuredHeight +
+ child.marginTop +
+ child.marginBottom
+
+ content.measure(
+ contentWidthSpec,
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec))
+ )
+ }
+ setMeasuredDimension(
+ MeasureSpec.getSize(widthMeasureSpec),
+ minOf(
+ MeasureSpec.getSize(heightMeasureSpec),
+ paddingTop +
+ content.marginTop +
+ content.measuredHeight +
+ content.marginBottom +
+ paddingBottom
+ )
+ )
+ }
+
+ override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
+ // let the parent scroll
+ super.onNestedPreScroll(target, dx, dy, consumed, type)
+ // scroll ourselves, if recycler has not scrolled
+ val delta = dy - consumed[1]
+ if (delta > 0 && target is ScrollingView && !target.canScrollVertically(-1)) {
+ val preScrollY = scrollY
+ scrollBy(0, delta)
+ consumed[1] += scrollY - preScrollY
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
index de76a1d2..b8fbedbf 100644
--- a/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
+++ b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
@@ -45,6 +45,8 @@ import android.view.animation.AnimationUtils;
import android.widget.AbsListView;
import android.widget.OverScroller;
+import androidx.annotation.Nullable;
+import androidx.core.view.ScrollingView;
import androidx.recyclerview.widget.RecyclerView;
import com.android.intentresolver.R;
@@ -131,6 +133,9 @@ public class ResolverDrawerLayout extends ViewGroup {
private AbsListView mNestedListChild;
private RecyclerView mNestedRecyclerChild;
+ @Nullable
+ private final ScrollablePreviewFlingLogicDelegate mFlingLogicDelegate;
+
private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener =
new ViewTreeObserver.OnTouchModeChangeListener() {
@Override
@@ -167,6 +172,12 @@ public class ResolverDrawerLayout extends ViewGroup {
mIgnoreOffsetTopLimitViewId = a.getResourceId(
R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit, ID_NULL);
}
+ mFlingLogicDelegate =
+ a.getBoolean(
+ R.styleable.ResolverDrawerLayout_useScrollablePreviewNestedFlingLogic,
+ false)
+ ? new ScrollablePreviewFlingLogicDelegate() {}
+ : null;
a.recycle();
mScrollIndicatorDrawable = mContext.getDrawable(
@@ -832,6 +843,9 @@ public class ResolverDrawerLayout extends ViewGroup {
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+ if (mFlingLogicDelegate != null) {
+ return mFlingLogicDelegate.onNestedPreFling(this, target, velocityX, velocityY);
+ }
if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) {
smoothScrollTo(0, velocityY);
return true;
@@ -841,9 +855,12 @@ public class ResolverDrawerLayout extends ViewGroup {
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+ if (mFlingLogicDelegate != null) {
+ return mFlingLogicDelegate.onNestedFling(this, target, velocityX, velocityY, consumed);
+ }
// TODO: find a more suitable way to fix it.
// RecyclerView started reporting `consumed` as true whenever a scrolling is enabled,
- // previously the value was based whether the fling can be performed in given direction
+ // previously the value was based on whether the fling can be performed in given direction
// i.e. whether it is at the top or at the bottom. isRecyclerViewAtTheTop method is a
// workaround that restores the legacy functionality.
boolean shouldConsume = (Math.abs(velocityY) > mMinFlingVelocity)
@@ -885,6 +902,13 @@ public class ResolverDrawerLayout extends ViewGroup {
&& firstChild.getTop() >= recyclerView.getPaddingTop();
}
+ private static boolean isFlingTargetAtTop(View target) {
+ if (target instanceof ScrollingView) {
+ return !target.canScrollVertically(-1);
+ }
+ return false;
+ }
+
private boolean performAccessibilityActionCommon(int action) {
switch (action) {
case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
@@ -1299,4 +1323,74 @@ public class ResolverDrawerLayout extends ViewGroup {
}
return mMetricsLogger;
}
+
+ /**
+ * Controlled by
+ * {@link com.android.intentresolver.Flags#FLAG_SCROLLABLE_PREVIEW}
+ */
+ private interface ScrollablePreviewFlingLogicDelegate {
+ default boolean onNestedPreFling(
+ ResolverDrawerLayout drawer, View target, float velocityX, float velocityY) {
+ boolean shouldScroll = !drawer.getShowAtTop() && velocityY > drawer.mMinFlingVelocity
+ && drawer.mCollapseOffset != 0;
+ if (shouldScroll) {
+ drawer.smoothScrollTo(0, velocityY);
+ return true;
+ }
+ boolean shouldDismiss = (Math.abs(velocityY) > drawer.mMinFlingVelocity)
+ && velocityY < 0
+ && isFlingTargetAtTop(target);
+ if (shouldDismiss) {
+ if (drawer.getShowAtTop()) {
+ drawer.smoothScrollTo(drawer.mCollapsibleHeight, velocityY);
+ } else {
+ if (drawer.isDismissable()
+ && drawer.mCollapseOffset > drawer.mCollapsibleHeight) {
+ drawer.smoothScrollTo(drawer.mHeightUsed, velocityY);
+ drawer.mDismissOnScrollerFinished = true;
+ } else {
+ drawer.smoothScrollTo(drawer.mCollapsibleHeight, velocityY);
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ default boolean onNestedFling(
+ ResolverDrawerLayout drawer,
+ View target,
+ float velocityX,
+ float velocityY,
+ boolean consumed) {
+ // TODO: find a more suitable way to fix it.
+ // RecyclerView started reporting `consumed` as true whenever a scrolling is enabled,
+ // previously the value was based on whether the fling can be performed in given
+ // direction i.e. whether it is at the top or at the bottom. isRecyclerViewAtTheTop
+ // method is a workaround that restores the legacy functionality.
+ boolean shouldConsume = (Math.abs(velocityY) > drawer.mMinFlingVelocity) && !consumed;
+ if (shouldConsume) {
+ if (drawer.getShowAtTop()) {
+ if (drawer.isDismissable() && velocityY > 0) {
+ drawer.abortAnimation();
+ drawer.dismiss();
+ } else {
+ drawer.smoothScrollTo(
+ velocityY < 0 ? drawer.mCollapsibleHeight : 0, velocityY);
+ }
+ } else {
+ if (drawer.isDismissable()
+ && velocityY < 0
+ && drawer.mCollapseOffset > drawer.mCollapsibleHeight) {
+ drawer.smoothScrollTo(drawer.mHeightUsed, velocityY);
+ drawer.mDismissOnScrollerFinished = true;
+ } else {
+ drawer.smoothScrollTo(
+ velocityY > 0 ? 0 : drawer.mCollapsibleHeight, velocityY);
+ }
+ }
+ }
+ return shouldConsume;
+ }
+ }
}
diff --git a/java/tests/Android.bp b/java/tests/Android.bp
index 974b8a47..5244bf7b 100644
--- a/java/tests/Android.bp
+++ b/java/tests/Android.bp
@@ -51,6 +51,8 @@ android_test {
"mockito-target-minus-junit4",
"testables",
"truth-prebuilt",
+ "flag-junit",
+ "platform-test-annotations",
],
plugins: ["dagger2-compiler"],
test_suites: ["general-tests"],
diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
index c0b15b6d..bc9e521c 100644
--- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
+++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
@@ -17,6 +17,7 @@
package com.android.intentresolver;
import static android.app.Activity.RESULT_OK;
+
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.longClick;
@@ -24,10 +25,12 @@ import static androidx.test.espresso.action.ViewActions.swipeUp;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.hasSibling;
+import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_CHOOSER_TARGET;
import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_DEFAULT;
import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
@@ -35,9 +38,12 @@ import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_F
import static com.android.intentresolver.ChooserListAdapter.CALLER_TARGET_SCORE_BOOST;
import static com.android.intentresolver.ChooserListAdapter.SHORTCUT_TARGET_SCORE_BOOST;
import static com.android.intentresolver.MatcherUtils.first;
+
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+
import static junit.framework.Assert.assertNull;
+
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
@@ -82,6 +88,9 @@ import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserHandle;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.provider.DeviceConfig;
import android.service.chooser.ChooserAction;
import android.service.chooser.ChooserTarget;
@@ -190,11 +199,13 @@ public class UnbundledChooserActivityTest {
private static final int CONTENT_PREVIEW_FILE = 2;
private static final int CONTENT_PREVIEW_TEXT = 3;
-
@Rule(order = 0)
- public HiltAndroidRule mHiltAndroidRule = new HiltAndroidRule(this);
+ public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
@Rule(order = 1)
+ public HiltAndroidRule mHiltAndroidRule = new HiltAndroidRule(this);
+
+ @Rule(order = 2)
public ActivityTestRule<ChooserWrapperActivity> mActivityRule =
new ActivityTestRule<>(ChooserWrapperActivity.class, false, false);
@@ -2160,6 +2171,42 @@ public class UnbundledChooserActivityTest {
.check(matches(isDisplayed()));
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_SCROLLABLE_PREVIEW)
+ public void testWorkTab_previewIsScrollable() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(300);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+
+ Uri uri = createTestContentProviderUri("image/png", null);
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createWideBitmap());
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Scrollable preview test"));
+ waitForIdle();
+
+ onView(withId(com.android.intentresolver.R.id.scrollable_image_preview))
+ .check(matches(isDisplayed()));
+
+ onView(withId(com.android.internal.R.id.contentPanel)).perform(swipeUp());
+ waitForIdle();
+
+ onView(withId(com.android.intentresolver.R.id.chooser_headline_row_container))
+ .check(matches(isCompletelyDisplayed()));
+ onView(withId(com.android.intentresolver.R.id.headline))
+ .check(matches(isDisplayed()));
+ onView(withId(com.android.intentresolver.R.id.scrollable_image_preview))
+ .check(matches(not(isDisplayed())));
+ }
+
@Ignore // b/220067877
@Test
public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() {
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/FileContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/FileContentPreviewUiTest.kt
index 6409da8a..d2d952ae 100644
--- a/java/tests/src/com/android/intentresolver/contentpreview/FileContentPreviewUiTest.kt
+++ b/java/tests/src/com/android/intentresolver/contentpreview/FileContentPreviewUiTest.kt
@@ -17,6 +17,7 @@
package com.android.intentresolver.contentpreview
import android.view.LayoutInflater
+import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -25,7 +26,7 @@ import com.android.intentresolver.R
import com.android.intentresolver.mock
import com.android.intentresolver.whenever
import com.android.intentresolver.widget.ActionRow
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
import java.util.function.Consumer
import org.junit.Test
import org.junit.runner.RunWith
@@ -48,15 +49,15 @@ class FileContentPreviewUiTest {
private val context
get() = InstrumentationRegistry.getInstrumentation().context
+ private val testSubject =
+ FileContentPreviewUi(
+ fileCount,
+ actionFactory,
+ headlineGenerator,
+ )
+
@Test
fun test_display_titleIsDisplayed() {
- val testSubject =
- FileContentPreviewUi(
- fileCount,
- actionFactory,
- headlineGenerator,
- )
-
val layoutInflater = LayoutInflater.from(context)
val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup
@@ -68,9 +69,31 @@ class FileContentPreviewUiTest {
/*headlineViewParent=*/ null
)
- Truth.assertThat(previewView).isNotNull()
+ assertThat(previewView).isNotNull()
val headlineView = previewView?.findViewById<TextView>(R.id.headline)
- Truth.assertThat(headlineView).isNotNull()
- Truth.assertThat(headlineView?.text).isEqualTo(text)
+ assertThat(headlineView).isNotNull()
+ assertThat(headlineView?.text).isEqualTo(text)
+ }
+
+ @Test
+ fun test_displayWithExternalHeaderView() {
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout =
+ layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false)
+ as ViewGroup
+ val externalHeaderView =
+ gridLayout.requireViewById<View>(R.id.chooser_headline_row_container)
+
+ assertThat(externalHeaderView.findViewById<View>(R.id.headline)).isNull()
+
+ val previewView =
+ testSubject.display(context.resources, layoutInflater, gridLayout, externalHeaderView)
+
+ assertThat(previewView).isNotNull()
+ assertThat(previewView.findViewById<View>(R.id.headline)).isNull()
+
+ val headlineView = externalHeaderView.findViewById<TextView>(R.id.headline)
+ assertThat(headlineView).isNotNull()
+ assertThat(headlineView?.text).isEqualTo(text)
}
}
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt
index 1144e3c9..0976dbf1 100644
--- a/java/tests/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt
+++ b/java/tests/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt
@@ -18,6 +18,7 @@ package com.android.intentresolver.contentpreview
import android.net.Uri
import android.view.LayoutInflater
+import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.lifecycle.testing.TestLifecycleOwner
@@ -28,6 +29,7 @@ import com.android.intentresolver.mock
import com.android.intentresolver.whenever
import com.android.intentresolver.widget.ActionRow
import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
import java.util.function.Consumer
import org.junit.Test
import org.junit.runner.RunWith
@@ -74,6 +76,17 @@ class FilesPlusTextContentPreviewUiTest {
}
@Test
+ fun test_displayImagesPlusTextWithoutUriMetadataExternalHeader_showImagesHeadline() {
+ val sharedFileCount = 2
+ val (previewView, headerParent) = testLoadingExternalHeadline("image/*", sharedFileCount)
+
+ verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(headerParent, HEADLINE_IMAGES)
+ verifySharedText(previewView)
+ }
+
+ @Test
fun test_displayVideosPlusTextWithoutUriMetadata_showVideosHeadline() {
val sharedFileCount = 2
val previewView = testLoadingHeadline("video/*", sharedFileCount)
@@ -84,6 +97,17 @@ class FilesPlusTextContentPreviewUiTest {
}
@Test
+ fun test_displayVideosPlusTextWithoutUriMetadataExternalHeader_showVideosHeadline() {
+ val sharedFileCount = 2
+ val (previewView, headerParent) = testLoadingExternalHeadline("video/*", sharedFileCount)
+
+ verify(headlineGenerator, times(1)).getVideosHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(headerParent, HEADLINE_VIDEOS)
+ verifySharedText(previewView)
+ }
+
+ @Test
fun test_displayDocsPlusTextWithoutUriMetadata_showFilesHeadline() {
val sharedFileCount = 2
val previewView = testLoadingHeadline("application/pdf", sharedFileCount)
@@ -94,6 +118,18 @@ class FilesPlusTextContentPreviewUiTest {
}
@Test
+ fun test_displayDocsPlusTextWithoutUriMetadataExternalHeader_showFilesHeadline() {
+ val sharedFileCount = 2
+ val (previewView, headerParent) =
+ testLoadingExternalHeadline("application/pdf", sharedFileCount)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(headerParent, HEADLINE_FILES)
+ verifySharedText(previewView)
+ }
+
+ @Test
fun test_displayMixedContentPlusTextWithoutUriMetadata_showFilesHeadline() {
val sharedFileCount = 2
val previewView = testLoadingHeadline("*/*", sharedFileCount)
@@ -104,6 +140,17 @@ class FilesPlusTextContentPreviewUiTest {
}
@Test
+ fun test_displayMixedContentPlusTextWithoutUriMetadataExternalHeader_showFilesHeadline() {
+ val sharedFileCount = 2
+ val (previewView, headerParent) = testLoadingExternalHeadline("*/*", sharedFileCount)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(headerParent, HEADLINE_FILES)
+ verifySharedText(previewView)
+ }
+
+ @Test
fun test_displayImagesPlusTextWithUriMetadataSet_showImagesHeadline() {
val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "image/jpeg")
val sharedFileCount = loadedFileMetadata.size
@@ -115,6 +162,19 @@ class FilesPlusTextContentPreviewUiTest {
}
@Test
+ fun test_displayImagesPlusTextWithUriMetadataSetExternalHeader_showImagesHeadline() {
+ val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "image/jpeg")
+ val sharedFileCount = loadedFileMetadata.size
+ val (previewView, headerParent) =
+ testLoadingExternalHeadline("image/*", sharedFileCount, loadedFileMetadata)
+
+ verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(headerParent, HEADLINE_IMAGES)
+ verifySharedText(previewView)
+ }
+
+ @Test
fun test_displayVideosPlusTextWithUriMetadataSet_showVideosHeadline() {
val loadedFileMetadata = createFileInfosWithMimeTypes("video/mp4", "video/mp4")
val sharedFileCount = loadedFileMetadata.size
@@ -126,6 +186,19 @@ class FilesPlusTextContentPreviewUiTest {
}
@Test
+ fun test_displayVideosPlusTextWithUriMetadataSetExternalHeader_showVideosHeadline() {
+ val loadedFileMetadata = createFileInfosWithMimeTypes("video/mp4", "video/mp4")
+ val sharedFileCount = loadedFileMetadata.size
+ val (previewView, headerParent) =
+ testLoadingExternalHeadline("video/*", sharedFileCount, loadedFileMetadata)
+
+ verify(headlineGenerator, times(1)).getVideosHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(headerParent, HEADLINE_VIDEOS)
+ verifySharedText(previewView)
+ }
+
+ @Test
fun test_displayImagesAndVideosPlusTextWithUriMetadataSet_showFilesHeadline() {
val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "video/mp4")
val sharedFileCount = loadedFileMetadata.size
@@ -137,6 +210,19 @@ class FilesPlusTextContentPreviewUiTest {
}
@Test
+ fun test_displayImagesAndVideosPlusTextWithUriMetadataSetExternalHeader_showFilesHeadline() {
+ val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "video/mp4")
+ val sharedFileCount = loadedFileMetadata.size
+ val (previewView, headerParent) =
+ testLoadingExternalHeadline("*/*", sharedFileCount, loadedFileMetadata)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(headerParent, HEADLINE_FILES)
+ verifySharedText(previewView)
+ }
+
+ @Test
fun test_displayDocsPlusTextWithUriMetadataSet_showFilesHeadline() {
val loadedFileMetadata = createFileInfosWithMimeTypes("application/pdf", "application/pdf")
val sharedFileCount = loadedFileMetadata.size
@@ -149,6 +235,19 @@ class FilesPlusTextContentPreviewUiTest {
}
@Test
+ fun test_displayDocsPlusTextWithUriMetadataSetExternalHeader_showFilesHeadline() {
+ val loadedFileMetadata = createFileInfosWithMimeTypes("application/pdf", "application/pdf")
+ val sharedFileCount = loadedFileMetadata.size
+ val (previewView, headerParent) =
+ testLoadingExternalHeadline("application/pdf", sharedFileCount, loadedFileMetadata)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(headerParent, HEADLINE_FILES)
+ verifySharedText(previewView)
+ }
+
+ @Test
fun test_uriMetadataIsMoreSpecificThanIntentMimeType_headlineGetsUpdated() {
val sharedFileCount = 2
val testSubject =
@@ -180,10 +279,56 @@ class FilesPlusTextContentPreviewUiTest {
verifyPreviewHeadline(previewView, HEADLINE_IMAGES)
}
+ @Test
+ fun test_uriMetadataIsMoreSpecificThanIntentMimeTypeExternalHeader_headlineGetsUpdated() {
+ val sharedFileCount = 2
+ val testSubject =
+ FilesPlusTextContentPreviewUi(
+ lifecycleOwner.lifecycle,
+ /*isSingleImage=*/ false,
+ sharedFileCount,
+ SHARED_TEXT,
+ /*intentMimeType=*/ "*/*",
+ actionFactory,
+ imageLoader,
+ DefaultMimeTypeClassifier,
+ headlineGenerator
+ )
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout =
+ layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false)
+ as ViewGroup
+ val externalHeaderView =
+ gridLayout.requireViewById<View>(R.id.chooser_headline_row_container)
+
+ assertWithMessage("External headline should not be inflated by default")
+ .that(externalHeaderView.findViewById<View>(R.id.headline))
+ .isNull()
+
+ val previewView =
+ testSubject.display(
+ context.resources,
+ LayoutInflater.from(context),
+ gridLayout,
+ externalHeaderView
+ )
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verify(headlineGenerator, never()).getImagesHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(externalHeaderView, HEADLINE_FILES)
+
+ testSubject.updatePreviewMetadata(createFileInfosWithMimeTypes("image/png", "image/jpg"))
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
+ verifyPreviewHeadline(externalHeaderView, HEADLINE_IMAGES)
+ }
+
private fun testLoadingHeadline(
intentMimeType: String,
sharedFileCount: Int,
- loadedFileMetadata: List<FileInfo>? = null
+ loadedFileMetadata: List<FileInfo>? = null,
): ViewGroup? {
val testSubject =
FilesPlusTextContentPreviewUi(
@@ -209,14 +354,51 @@ class FilesPlusTextContentPreviewUiTest {
)
}
+ private fun testLoadingExternalHeadline(
+ intentMimeType: String,
+ sharedFileCount: Int,
+ loadedFileMetadata: List<FileInfo>? = null,
+ ): Pair<ViewGroup?, View> {
+ val testSubject =
+ FilesPlusTextContentPreviewUi(
+ lifecycleOwner.lifecycle,
+ /*isSingleImage=*/ false,
+ sharedFileCount,
+ SHARED_TEXT,
+ intentMimeType,
+ actionFactory,
+ imageLoader,
+ DefaultMimeTypeClassifier,
+ headlineGenerator
+ )
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout =
+ layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false)
+ as ViewGroup
+ val externalHeaderView =
+ gridLayout.requireViewById<View>(R.id.chooser_headline_row_container)
+
+ assertWithMessage("External headline should not be inflated by default")
+ .that(externalHeaderView.findViewById<View>(R.id.headline))
+ .isNull()
+
+ loadedFileMetadata?.let(testSubject::updatePreviewMetadata)
+ return testSubject.display(
+ context.resources,
+ LayoutInflater.from(context),
+ gridLayout,
+ externalHeaderView
+ ) to externalHeaderView
+ }
+
private fun createFileInfosWithMimeTypes(vararg mimeTypes: String): List<FileInfo> {
val uri = Uri.parse("content://pkg.app/file")
return mimeTypes.map { mimeType -> FileInfo.Builder(uri).withMimeType(mimeType).build() }
}
- private fun verifyPreviewHeadline(previewView: ViewGroup?, expectedText: String) {
- assertThat(previewView).isNotNull()
- val headlineView = previewView?.findViewById<TextView>(R.id.headline)
+ private fun verifyPreviewHeadline(headerViewParent: View?, expectedText: String) {
+ assertThat(headerViewParent).isNotNull()
+ val headlineView = headerViewParent?.findViewById<TextView>(R.id.headline)
assertThat(headlineView).isNotNull()
assertThat(headlineView?.text).isEqualTo(expectedText)
}
@@ -227,4 +409,13 @@ class FilesPlusTextContentPreviewUiTest {
assertThat(textContentView).isNotNull()
assertThat(textContentView?.text).isEqualTo(SHARED_TEXT)
}
+
+ private fun verifyInternalHeadlineAbsence(previewView: ViewGroup?) {
+ assertWithMessage("Preview parent should not be null").that(previewView).isNotNull()
+ assertWithMessage(
+ "Preview headline should not be inflated when an external headline is used"
+ )
+ .that(previewView?.findViewById<View>(R.id.headline))
+ .isNull()
+ }
}
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/TextContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/TextContentPreviewUiTest.kt
index 69053f73..b91ed436 100644
--- a/java/tests/src/com/android/intentresolver/contentpreview/TextContentPreviewUiTest.kt
+++ b/java/tests/src/com/android/intentresolver/contentpreview/TextContentPreviewUiTest.kt
@@ -17,6 +17,7 @@
package com.android.intentresolver.contentpreview
import android.view.LayoutInflater
+import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.lifecycle.testing.TestLifecycleOwner
@@ -51,18 +52,19 @@ class TextContentPreviewUiTest {
private val context
get() = InstrumentationRegistry.getInstrumentation().context
+ private val testSubject =
+ TextContentPreviewUi(
+ lifecycleOwner.lifecycle,
+ text,
+ title,
+ /*previewThumbnail=*/ null,
+ actionFactory,
+ imageLoader,
+ headlineGenerator,
+ )
+
@Test
fun test_display_headlineIsDisplayed() {
- val testSubject =
- TextContentPreviewUi(
- lifecycleOwner.lifecycle,
- text,
- title,
- /*previewThumbnail=*/ null,
- actionFactory,
- imageLoader,
- headlineGenerator,
- )
val layoutInflater = LayoutInflater.from(context)
val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup
@@ -79,4 +81,26 @@ class TextContentPreviewUiTest {
assertThat(headlineView).isNotNull()
assertThat(headlineView?.text).isEqualTo(text)
}
+
+ @Test
+ fun test_displayWithExternalHeaderView_externalHeaderIsDisplayed() {
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout =
+ layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false)
+ as ViewGroup
+ val externalHeaderView =
+ gridLayout.requireViewById<View>(R.id.chooser_headline_row_container)
+
+ assertThat(externalHeaderView.findViewById<View>(R.id.headline)).isNull()
+
+ val previewView =
+ testSubject.display(context.resources, layoutInflater, gridLayout, externalHeaderView)
+
+ assertThat(previewView).isNotNull()
+ assertThat(previewView.findViewById<View>(R.id.headline)).isNull()
+
+ val headlineView = externalHeaderView.findViewById<TextView>(R.id.headline)
+ assertThat(headlineView).isNotNull()
+ assertThat(headlineView?.text).isEqualTo(text)
+ }
}
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt
index 6b22b850..7e07e0ca 100644
--- a/java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt
+++ b/java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt
@@ -18,13 +18,17 @@ package com.android.intentresolver.contentpreview
import android.net.Uri
import android.view.LayoutInflater
+import android.view.View
import android.view.ViewGroup
+import android.widget.TextView
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.R
import com.android.intentresolver.mock
import com.android.intentresolver.whenever
import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback
+import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertWithMessage
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asFlow
@@ -38,6 +42,10 @@ import org.mockito.Mockito.anyInt
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
+private const val IMAGE_HEADLINE = "Image Headline"
+private const val VIDEO_HEADLINE = "Video Headline"
+private const val FILES_HEADLINE = "Files Headline"
+
@RunWith(AndroidJUnit4::class)
class UnifiedContentPreviewUiTest {
private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher())
@@ -48,40 +56,76 @@ class UnifiedContentPreviewUiTest {
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")
+ whenever(getImagesHeadline(anyInt())).thenReturn(IMAGE_HEADLINE)
+ whenever(getVideosHeadline(anyInt())).thenReturn(VIDEO_HEADLINE)
+ whenever(getFilesHeadline(anyInt())).thenReturn(FILES_HEADLINE)
}
private val context
- get() = getInstrumentation().getContext()
+ get() = getInstrumentation().context
@Test
fun test_displayImagesWithoutUriMetadata_showImagesHeadline() {
- testLoadingHeadline("image/*", files = null)
+ testLoadingHeadline("image/*", files = null) { previewView ->
+ verify(headlineGenerator, times(1)).getImagesHeadline(2)
+ verifyPreviewHeadline(previewView, IMAGE_HEADLINE)
+ }
+ }
- verify(headlineGenerator, times(1)).getImagesHeadline(2)
+ @Test
+ fun test_displayImagesWithoutUriMetadataExternalHeader_showImagesHeadline() {
+ testLoadingExternalHeadline("image/*", files = null) { externalHeaderView ->
+ verify(headlineGenerator, times(1)).getImagesHeadline(2)
+ verifyPreviewHeadline(externalHeaderView, IMAGE_HEADLINE)
+ }
}
@Test
fun test_displayVideosWithoutUriMetadata_showImagesHeadline() {
- testLoadingHeadline("video/*", files = null)
+ testLoadingHeadline("video/*", files = null) { previewView ->
+ verify(headlineGenerator, times(1)).getVideosHeadline(2)
+ verifyPreviewHeadline(previewView, VIDEO_HEADLINE)
+ }
+ }
- verify(headlineGenerator, times(1)).getVideosHeadline(2)
+ @Test
+ fun test_displayVideosWithoutUriMetadataExternalHeader_showImagesHeadline() {
+ testLoadingExternalHeadline("video/*", files = null) { externalHeaderView ->
+ verify(headlineGenerator, times(1)).getVideosHeadline(2)
+ verifyPreviewHeadline(externalHeaderView, VIDEO_HEADLINE)
+ }
}
@Test
fun test_displayDocumentsWithoutUriMetadata_showImagesHeadline() {
- testLoadingHeadline("application/pdf", files = null)
+ testLoadingHeadline("application/pdf", files = null) { previewView ->
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ verifyPreviewHeadline(previewView, FILES_HEADLINE)
+ }
+ }
- verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ @Test
+ fun test_displayDocumentsWithoutUriMetadataExternalHeader_showImagesHeadline() {
+ testLoadingExternalHeadline("application/pdf", files = null) { externalHeaderView ->
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ verifyPreviewHeadline(externalHeaderView, FILES_HEADLINE)
+ }
}
@Test
fun test_displayMixedContentWithoutUriMetadata_showImagesHeadline() {
- testLoadingHeadline("*/*", files = null)
+ testLoadingHeadline("*/*", files = null) { previewView ->
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ verifyPreviewHeadline(previewView, FILES_HEADLINE)
+ }
+ }
- verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ @Test
+ fun test_displayMixedContentWithoutUriMetadataExternalHeader_showImagesHeadline() {
+ testLoadingExternalHeadline("*/*", files = null) { externalHeader ->
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ verifyPreviewHeadline(externalHeader, FILES_HEADLINE)
+ }
}
@Test
@@ -92,9 +136,24 @@ class UnifiedContentPreviewUiTest {
FileInfo.Builder(uri).withMimeType("image/png").build(),
FileInfo.Builder(uri).withMimeType("image/jpeg").build(),
)
- testLoadingHeadline("image/*", files)
+ testLoadingHeadline("image/*", files) { preivewView ->
+ verify(headlineGenerator, times(1)).getImagesHeadline(2)
+ verifyPreviewHeadline(preivewView, IMAGE_HEADLINE)
+ }
+ }
- verify(headlineGenerator, times(1)).getImagesHeadline(2)
+ @Test
+ fun test_displayImagesWithUriMetadataSetExternalHeader_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(),
+ )
+ testLoadingExternalHeadline("image/*", files) { externalHeader ->
+ verify(headlineGenerator, times(1)).getImagesHeadline(2)
+ verifyPreviewHeadline(externalHeader, IMAGE_HEADLINE)
+ }
}
@Test
@@ -105,9 +164,24 @@ class UnifiedContentPreviewUiTest {
FileInfo.Builder(uri).withMimeType("video/mp4").build(),
FileInfo.Builder(uri).withMimeType("video/mp4").build(),
)
- testLoadingHeadline("video/*", files)
+ testLoadingHeadline("video/*", files) { previewView ->
+ verify(headlineGenerator, times(1)).getVideosHeadline(2)
+ verifyPreviewHeadline(previewView, VIDEO_HEADLINE)
+ }
+ }
- verify(headlineGenerator, times(1)).getVideosHeadline(2)
+ @Test
+ fun test_displayVideosWithUriMetadataSetExternalHeader_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(),
+ )
+ testLoadingExternalHeadline("video/*", files) { externalHeader ->
+ verify(headlineGenerator, times(1)).getVideosHeadline(2)
+ verifyPreviewHeadline(externalHeader, VIDEO_HEADLINE)
+ }
}
@Test
@@ -118,9 +192,24 @@ class UnifiedContentPreviewUiTest {
FileInfo.Builder(uri).withMimeType("image/png").build(),
FileInfo.Builder(uri).withMimeType("video/mp4").build(),
)
- testLoadingHeadline("*/*", files)
+ testLoadingHeadline("*/*", files) { previewView ->
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ verifyPreviewHeadline(previewView, FILES_HEADLINE)
+ }
+ }
- verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ @Test
+ fun test_displayImagesAndVideosWithUriMetadataSetExternalHeader_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(),
+ )
+ testLoadingExternalHeadline("*/*", files) { externalHeader ->
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ verifyPreviewHeadline(externalHeader, FILES_HEADLINE)
+ }
}
@Test
@@ -131,12 +220,31 @@ class UnifiedContentPreviewUiTest {
FileInfo.Builder(uri).withMimeType("application/pdf").build(),
FileInfo.Builder(uri).withMimeType("application/pdf").build(),
)
- testLoadingHeadline("application/pdf", files)
+ testLoadingHeadline("application/pdf", files) { previewView ->
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ verifyPreviewHeadline(previewView, FILES_HEADLINE)
+ }
+ }
- verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ @Test
+ fun test_displayDocumentsWithUriMetadataSetExternalHeader_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(),
+ )
+ testLoadingExternalHeadline("application/pdf", files) { externalHeader ->
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ verifyPreviewHeadline(externalHeader, FILES_HEADLINE)
+ }
}
- private fun testLoadingHeadline(intentMimeType: String, files: List<FileInfo>?) {
+ private fun testLoadingHeadline(
+ intentMimeType: String,
+ files: List<FileInfo>?,
+ verificationBlock: (ViewGroup?) -> Unit,
+ ) {
testScope.runTest {
val endMarker = FileInfo.Builder(Uri.EMPTY).build()
val emptySourceFlow = MutableSharedFlow<FileInfo>(replay = 1)
@@ -157,15 +265,84 @@ class UnifiedContentPreviewUiTest {
headlineGenerator
)
val layoutInflater = LayoutInflater.from(context)
- val gridLayout = layoutInflater.inflate(chooser_grid, null, false) as ViewGroup
+ val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup
- testSubject.display(
- context.resources,
- LayoutInflater.from(context),
- gridLayout,
- /*headlineViewParent=*/ null
- )
+ val previewView =
+ testSubject.display(
+ context.resources,
+ LayoutInflater.from(context),
+ gridLayout,
+ /*headlineViewParent=*/ null
+ )
emptySourceFlow.tryEmit(endMarker)
+
+ verificationBlock(previewView)
}
}
+
+ private fun testLoadingExternalHeadline(
+ intentMimeType: String,
+ files: List<FileInfo>?,
+ verificationBlock: (View?) -> Unit,
+ ) {
+ testScope.runTest {
+ val endMarker = FileInfo.Builder(Uri.EMPTY).build()
+ val emptySourceFlow = MutableSharedFlow<FileInfo>(replay = 1)
+ val testSubject =
+ UnifiedContentPreviewUi(
+ testScope,
+ /*isSingleImage=*/ false,
+ intentMimeType,
+ actionFactory,
+ imageLoader,
+ DefaultMimeTypeClassifier,
+ object : TransitionElementStatusCallback {
+ override fun onTransitionElementReady(name: String) = Unit
+ override fun onAllTransitionElementsReady() = Unit
+ },
+ files?.let { it.asFlow() } ?: emptySourceFlow.takeWhile { it !== endMarker },
+ /*itemCount=*/ 2,
+ headlineGenerator
+ )
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout =
+ layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false)
+ as ViewGroup
+ val externalHeaderView =
+ gridLayout.requireViewById<View>(R.id.chooser_headline_row_container)
+
+ assertWithMessage("External headline should not be inflated by default")
+ .that(externalHeaderView.findViewById<View>(R.id.headline))
+ .isNull()
+
+ val previewView =
+ testSubject.display(
+ context.resources,
+ LayoutInflater.from(context),
+ gridLayout,
+ externalHeaderView,
+ )
+
+ emptySourceFlow.tryEmit(endMarker)
+
+ verifyInternalHeadlineAbsence(previewView)
+ verificationBlock(externalHeaderView)
+ }
+ }
+
+ private fun verifyPreviewHeadline(headerViewParent: View?, expectedText: String) {
+ Truth.assertThat(headerViewParent).isNotNull()
+ val headlineView = headerViewParent?.findViewById<TextView>(R.id.headline)
+ Truth.assertThat(headlineView).isNotNull()
+ Truth.assertThat(headlineView?.text).isEqualTo(expectedText)
+ }
+
+ private fun verifyInternalHeadlineAbsence(previewView: ViewGroup?) {
+ assertWithMessage("Preview parent should not be null").that(previewView).isNotNull()
+ assertWithMessage(
+ "Preview headline should not be inflated when an external headline is used"
+ )
+ .that(previewView?.findViewById<View>(R.id.headline))
+ .isNull()
+ }
}