summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp2
-rw-r--r--java/res/layout/chooser_dialog.xml4
-rw-r--r--java/res/layout/chooser_grid.xml11
-rw-r--r--java/res/layout/chooser_list_per_profile.xml5
-rw-r--r--java/res/layout/miniresolver.xml19
-rw-r--r--java/res/layout/resolver_different_item_header.xml3
-rw-r--r--java/res/layout/resolver_list.xml21
-rw-r--r--java/res/layout/resolver_list_with_default.xml19
-rw-r--r--java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java5
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java9
-rw-r--r--java/src/com/android/intentresolver/ChooserGridLayoutManager.java4
-rw-r--r--java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java7
-rw-r--r--java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java4
-rw-r--r--java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java4
-rw-r--r--java/src/com/android/intentresolver/ResolverActivity.java5
-rw-r--r--java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java3
-rw-r--r--java/src/com/android/intentresolver/ResolverViewPager.java11
-rw-r--r--java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java1223
-rw-r--r--java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java4
19 files changed, 1295 insertions, 68 deletions
diff --git a/Android.bp b/Android.bp
index 2407fc72..a8e64863 100644
--- a/Android.bp
+++ b/Android.bp
@@ -45,6 +45,8 @@ android_library {
static_libs: [
"androidx.annotation_annotation",
+ "androidx.recyclerview_recyclerview",
+ "androidx.viewpager_viewpager",
"unsupportedappusage",
],
diff --git a/java/res/layout/chooser_dialog.xml b/java/res/layout/chooser_dialog.xml
index ff66bbb9..e31712c7 100644
--- a/java/res/layout/chooser_dialog.xml
+++ b/java/res/layout/chooser_dialog.xml
@@ -50,9 +50,9 @@
</LinearLayout>
- <com.android.internal.widget.RecyclerView
+ <androidx.recyclerview.widget.RecyclerView
xmlns:app="http://schemas.android.com/apk/res-auto"
- androidprv:layoutManager="com.android.internal.widget.LinearLayoutManager"
+ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:id="@androidprv:id/listContainer"
android:overScrollMode="never"
android:layout_width="match_parent"
diff --git a/java/res/layout/chooser_grid.xml b/java/res/layout/chooser_grid.xml
index a95b0ebe..d863495d 100644
--- a/java/res/layout/chooser_grid.xml
+++ b/java/res/layout/chooser_grid.xml
@@ -16,14 +16,15 @@
* limitations under the License.
*/
-->
-<com.android.internal.widget.ResolverDrawerLayout
+<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"
- androidprv:maxCollapsedHeight="0dp"
- androidprv:maxCollapsedHeightSmall="56dp"
+ app:maxCollapsedHeight="0dp"
+ app:maxCollapsedHeightSmall="56dp"
android:maxWidth="@dimen/chooser_width"
android:id="@androidprv:id/contentPanel">
@@ -31,7 +32,7 @@
android:id="@androidprv:id/chooser_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- androidprv:layout_alwaysShow="true"
+ app:layout_alwaysShow="true"
android:elevation="0dp"
android:background="@drawable/bottomsheet_background">
@@ -94,4 +95,4 @@
</LinearLayout>
</TabHost>
-</com.android.internal.widget.ResolverDrawerLayout>
+</com.android.intentresolver.widget.ResolverDrawerLayout>
diff --git a/java/res/layout/chooser_list_per_profile.xml b/java/res/layout/chooser_list_per_profile.xml
index 8d876cdf..1753e2f6 100644
--- a/java/res/layout/chooser_list_per_profile.xml
+++ b/java/res/layout/chooser_list_per_profile.xml
@@ -16,12 +16,13 @@
<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="match_parent">
- <com.android.internal.widget.RecyclerView
+ <androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
- androidprv:layoutManager="com.android.intentresolver.ChooserGridLayoutManager"
+ app:layoutManager="com.android.intentresolver.ChooserGridLayoutManager"
android:id="@androidprv:id/resolver_list"
android:clipToPadding="false"
android:background="?android:attr/colorBackground"
diff --git a/java/res/layout/miniresolver.xml b/java/res/layout/miniresolver.xml
index ab65aa9b..7e31de57 100644
--- a/java/res/layout/miniresolver.xml
+++ b/java/res/layout/miniresolver.xml
@@ -14,20 +14,21 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<com.android.internal.widget.ResolverDrawerLayout
+<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:maxWidth="@dimen/resolver_max_width"
- androidprv:maxCollapsedHeight="@dimen/resolver_max_collapsed_height"
- androidprv:maxCollapsedHeightSmall="56dp"
+ app:maxCollapsedHeight="@dimen/resolver_max_collapsed_height"
+ app:maxCollapsedHeightSmall="56dp"
android:id="@androidprv:id/contentPanel">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- androidprv:layout_alwaysShow="true"
+ app:layout_alwaysShow="true"
android:elevation="@dimen/resolver_elevation"
android:paddingTop="24dp"
android:paddingStart="@dimen/resolver_edge_margin"
@@ -62,18 +63,18 @@
android:id="@androidprv:id/button_bar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- androidprv:layout_alwaysShow="true"
+ app:layout_alwaysShow="true"
android:paddingTop="32dp"
android:paddingBottom="@dimen/resolver_button_bar_spacing"
android:orientation="vertical"
android:background="?android:attr/colorBackground"
- androidprv:layout_ignoreOffset="true">
+ app:layout_ignoreOffset="true">
<RelativeLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- androidprv:layout_ignoreOffset="true"
- androidprv:layout_hasNestedScrollIndicator="true"
+ app:layout_ignoreOffset="true"
+ app:layout_hasNestedScrollIndicator="true"
android:gravity="end|center_vertical"
android:orientation="horizontal"
android:layoutDirection="locale"
@@ -112,4 +113,4 @@
/>
</RelativeLayout>
</LinearLayout>
-</com.android.internal.widget.ResolverDrawerLayout>
+</com.android.intentresolver.widget.ResolverDrawerLayout>
diff --git a/java/res/layout/resolver_different_item_header.xml b/java/res/layout/resolver_different_item_header.xml
index 4f801597..79ce6824 100644
--- a/java/res/layout/resolver_different_item_header.xml
+++ b/java/res/layout/resolver_different_item_header.xml
@@ -19,9 +19,10 @@
<TextView
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"
- androidprv:layout_alwaysShow="true"
+ app:layout_alwaysShow="true"
android:text="@string/use_a_different_app"
android:textColor="?android:attr/textColorPrimary"
android:fontFamily="@androidprv:string/config_headlineFontFamilyMedium"
diff --git a/java/res/layout/resolver_list.xml b/java/res/layout/resolver_list.xml
index 179c4073..44b14baf 100644
--- a/java/res/layout/resolver_list.xml
+++ b/java/res/layout/resolver_list.xml
@@ -16,21 +16,22 @@
* limitations under the License.
*/
-->
-<com.android.internal.widget.ResolverDrawerLayout
+<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:maxWidth="@dimen/resolver_max_width"
- androidprv:maxCollapsedHeight="@dimen/resolver_max_collapsed_height"
- androidprv:maxCollapsedHeightSmall="56dp"
+ app:maxCollapsedHeight="@dimen/resolver_max_collapsed_height"
+ app:maxCollapsedHeightSmall="56dp"
android:id="@androidprv:id/contentPanel">
<RelativeLayout
android:id="@androidprv:id/title_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- androidprv:layout_alwaysShow="true"
+ app:layout_alwaysShow="true"
android:elevation="@dimen/resolver_elevation"
android:paddingTop="@dimen/resolver_small_margin"
android:paddingStart="@dimen/resolver_edge_margin"
@@ -66,7 +67,7 @@
<View
android:id="@androidprv:id/divider"
- androidprv:layout_alwaysShow="true"
+ app:layout_alwaysShow="true"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/colorBackground"
@@ -114,10 +115,10 @@
android:id="@androidprv:id/button_bar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- androidprv:layout_alwaysShow="true"
+ app:layout_alwaysShow="true"
android:orientation="vertical"
android:background="?android:attr/colorBackground"
- androidprv:layout_ignoreOffset="true">
+ app:layout_ignoreOffset="true">
<View
android:id="@androidprv:id/resolver_button_bar_divider"
android:layout_width="match_parent"
@@ -130,8 +131,8 @@
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- androidprv:layout_ignoreOffset="true"
- androidprv:layout_hasNestedScrollIndicator="true"
+ app:layout_ignoreOffset="true"
+ app:layout_hasNestedScrollIndicator="true"
android:gravity="end|center_vertical"
android:orientation="horizontal"
android:layoutDirection="locale"
@@ -169,4 +170,4 @@
android:onClick="onButtonClick" />
</LinearLayout>
</LinearLayout>
-</com.android.internal.widget.ResolverDrawerLayout>
+</com.android.intentresolver.widget.ResolverDrawerLayout>
diff --git a/java/res/layout/resolver_list_with_default.xml b/java/res/layout/resolver_list_with_default.xml
index 341c58e7..192a5983 100644
--- a/java/res/layout/resolver_list_with_default.xml
+++ b/java/res/layout/resolver_list_with_default.xml
@@ -16,19 +16,20 @@
* limitations under the License.
*/
-->
-<com.android.internal.widget.ResolverDrawerLayout
+<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:maxWidth="@dimen/resolver_max_width"
- androidprv:maxCollapsedHeight="@dimen/resolver_max_collapsed_height_with_default"
+ app:maxCollapsedHeight="@dimen/resolver_max_collapsed_height_with_default"
android:id="@androidprv:id/contentPanel">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- androidprv:layout_alwaysShow="true"
+ app:layout_alwaysShow="true"
android:orientation="vertical"
android:background="@drawable/bottomsheet_background"
android:paddingTop="@dimen/resolver_small_margin"
@@ -105,7 +106,7 @@
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- androidprv:layout_alwaysShow="true"
+ app:layout_alwaysShow="true"
android:gravity="end|center_vertical"
android:orientation="horizontal"
android:layoutDirection="locale"
@@ -146,7 +147,7 @@
<View
android:id="@androidprv:id/divider"
- androidprv:layout_alwaysShow="true"
+ app:layout_alwaysShow="true"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/colorBackground"
@@ -154,14 +155,14 @@
<FrameLayout
android:id="@androidprv:id/stub"
- androidprv:layout_alwaysShow="true"
+ app:layout_alwaysShow="true"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/colorBackground"/>
<TabHost
- androidprv:layout_alwaysShow="true"
+ app:layout_alwaysShow="true"
android:id="@androidprv:id/profile_tabhost"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -198,9 +199,9 @@
</TabHost>
<View
- androidprv:layout_alwaysShow="true"
+ app:layout_alwaysShow="true"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/colorBackground"
android:foreground="?android:attr/dividerVertical" />
-</com.android.internal.widget.ResolverDrawerLayout>
+</com.android.intentresolver.widget.ResolverDrawerLayout>
diff --git a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
index 4f6c0bf1..0f0d1797 100644
--- a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
@@ -34,9 +34,10 @@ import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
+import androidx.viewpager.widget.PagerAdapter;
+import androidx.viewpager.widget.ViewPager;
+
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.widget.PagerAdapter;
-import com.android.internal.widget.ViewPager;
import java.util.HashSet;
import java.util.List;
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index d0e47562..36b32f6a 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -112,6 +112,10 @@ import android.widget.ImageView;
import android.widget.Space;
import android.widget.TextView;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.viewpager.widget.ViewPager;
+
import com.android.intentresolver.ResolverListAdapter.ActivityInfoPresentationGetter;
import com.android.intentresolver.ResolverListAdapter.ViewHolder;
import com.android.intentresolver.chooser.ChooserTargetInfo;
@@ -121,16 +125,13 @@ import com.android.intentresolver.chooser.NotSelectableTargetInfo;
import com.android.intentresolver.chooser.SelectableTargetInfo;
import com.android.intentresolver.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.widget.ResolverDrawerLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.content.PackageMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.FrameworkStatsLog;
-import com.android.internal.widget.GridLayoutManager;
-import com.android.internal.widget.RecyclerView;
-import com.android.internal.widget.ResolverDrawerLayout;
-import com.android.internal.widget.ViewPager;
import com.google.android.collect.Lists;
diff --git a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
index 7c4b0c1f..5f373525 100644
--- a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
+++ b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
@@ -19,8 +19,8 @@ package com.android.intentresolver;
import android.content.Context;
import android.util.AttributeSet;
-import com.android.internal.widget.GridLayoutManager;
-import com.android.internal.widget.RecyclerView;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
/**
* For a11y and per {@link RecyclerView#onInitializeAccessibilityNodeInfo}, override
diff --git a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
index da78fc81..62c14866 100644
--- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
@@ -33,10 +33,11 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.viewpager.widget.PagerAdapter;
+
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.widget.GridLayoutManager;
-import com.android.internal.widget.PagerAdapter;
-import com.android.internal.widget.RecyclerView;
/**
* A {@link PagerAdapter} which describes the work and personal profile share sheet screens.
diff --git a/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
index 67571b44..250b6827 100644
--- a/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
+++ b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
@@ -22,8 +22,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
-import com.android.internal.widget.RecyclerView;
-import com.android.internal.widget.RecyclerViewAccessibilityDelegate;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
class ChooserRecyclerViewAccessibilityDelegate extends RecyclerViewAccessibilityDelegate {
private final Rect mTempRect = new Rect();
diff --git a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
index ffd173c7..b4a102ae 100644
--- a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
+++ b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
@@ -49,9 +49,9 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
-import com.android.intentresolver.chooser.DisplayResolveInfo;
+import androidx.recyclerview.widget.RecyclerView;
-import com.android.internal.widget.RecyclerView;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
import java.util.ArrayList;
import java.util.List;
diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java
index 6c013221..ea140dcb 100644
--- a/java/src/com/android/intentresolver/ResolverActivity.java
+++ b/java/src/com/android/intentresolver/ResolverActivity.java
@@ -91,17 +91,18 @@ import android.widget.TabWidget;
import android.widget.TextView;
import android.widget.Toast;
+import androidx.viewpager.widget.ViewPager;
+
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.Profile;
import com.android.intentresolver.chooser.ChooserTargetInfo;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.widget.ResolverDrawerLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
import com.android.internal.util.LatencyTracker;
-import com.android.internal.widget.ResolverDrawerLayout;
-import com.android.internal.widget.ViewPager;
import java.util.ArrayList;
import java.util.Arrays;
diff --git a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
index 56d326c1..7cd38a7e 100644
--- a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
@@ -32,8 +32,9 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
+import androidx.viewpager.widget.PagerAdapter;
+
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.widget.PagerAdapter;
/**
* A {@link PagerAdapter} which describes the work and personal profile intent resolver screens.
diff --git a/java/src/com/android/intentresolver/ResolverViewPager.java b/java/src/com/android/intentresolver/ResolverViewPager.java
index 1c234526..0804a2b8 100644
--- a/java/src/com/android/intentresolver/ResolverViewPager.java
+++ b/java/src/com/android/intentresolver/ResolverViewPager.java
@@ -21,7 +21,7 @@ import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
-import com.android.internal.widget.ViewPager;
+import androidx.viewpager.widget.ViewPager;
/**
* A {@link ViewPager} which wraps around its tallest child's height.
@@ -41,15 +41,6 @@ public class ResolverViewPager extends ViewPager {
super(context, attrs);
}
- public ResolverViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- public ResolverViewPager(Context context, AttributeSet attrs,
- int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
diff --git a/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
new file mode 100644
index 00000000..29821e66
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
@@ -0,0 +1,1223 @@
+/*
+ * Copyright (C) 2014 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.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.metrics.LogMaker;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.animation.AnimationUtils;
+import android.widget.AbsListView;
+import android.widget.OverScroller;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.intentresolver.R;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+public class ResolverDrawerLayout extends ViewGroup {
+ private static final String TAG = "ResolverDrawerLayout";
+ private MetricsLogger mMetricsLogger;
+
+ /**
+ * Max width of the whole drawer layout
+ */
+ private final int mMaxWidth;
+
+ /**
+ * Max total visible height of views not marked always-show when in the closed/initial state
+ */
+ private int mMaxCollapsedHeight;
+
+ /**
+ * Max total visible height of views not marked always-show when in the closed/initial state
+ * when a default option is present
+ */
+ private int mMaxCollapsedHeightSmall;
+
+ /**
+ * Whether {@code mMaxCollapsedHeightSmall} was set explicitly as a layout attribute or
+ * inferred by {@code mMaxCollapsedHeight}.
+ */
+ private final boolean mIsMaxCollapsedHeightSmallExplicit;
+
+ private boolean mSmallCollapsed;
+
+ /**
+ * Move views down from the top by this much in px
+ */
+ private float mCollapseOffset;
+
+ /**
+ * Track fractions of pixels from drag calculations. Without this, the view offsets get
+ * out of sync due to frequently dropping fractions of a pixel from '(int) dy' casts.
+ */
+ private float mDragRemainder = 0.0f;
+ private int mCollapsibleHeight;
+ private int mUncollapsibleHeight;
+ private int mAlwaysShowHeight;
+
+ /**
+ * The height in pixels of reserved space added to the top of the collapsed UI;
+ * e.g. chooser targets
+ */
+ private int mCollapsibleHeightReserved;
+
+ private int mTopOffset;
+ private boolean mShowAtTop;
+
+ private boolean mIsDragging;
+ private boolean mOpenOnClick;
+ private boolean mOpenOnLayout;
+ private boolean mDismissOnScrollerFinished;
+ private final int mTouchSlop;
+ private final float mMinFlingVelocity;
+ private final OverScroller mScroller;
+ private final VelocityTracker mVelocityTracker;
+
+ private Drawable mScrollIndicatorDrawable;
+
+ private OnDismissedListener mOnDismissedListener;
+ private RunOnDismissedListener mRunOnDismissedListener;
+ private OnCollapsedChangedListener mOnCollapsedChangedListener;
+
+ private boolean mDismissLocked;
+
+ private float mInitialTouchX;
+ private float mInitialTouchY;
+ private float mLastTouchY;
+ private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;
+
+ private final Rect mTempRect = new Rect();
+
+ private AbsListView mNestedListChild;
+ private RecyclerView mNestedRecyclerChild;
+
+ private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener =
+ new ViewTreeObserver.OnTouchModeChangeListener() {
+ @Override
+ public void onTouchModeChanged(boolean isInTouchMode) {
+ if (!isInTouchMode && hasFocus() && isDescendantClipped(getFocusedChild())) {
+ smoothScrollTo(0, 0);
+ }
+ }
+ };
+
+ public ResolverDrawerLayout(Context context) {
+ this(context, null);
+ }
+
+ public ResolverDrawerLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout,
+ defStyleAttr, 0);
+ mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_android_maxWidth, -1);
+ mMaxCollapsedHeight = a.getDimensionPixelSize(
+ R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0);
+ mMaxCollapsedHeightSmall = a.getDimensionPixelSize(
+ R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall,
+ mMaxCollapsedHeight);
+ mIsMaxCollapsedHeightSmallExplicit =
+ a.hasValue(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall);
+ mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false);
+ a.recycle();
+
+ mScrollIndicatorDrawable = mContext.getDrawable(
+ com.android.internal.R.drawable.scroll_indicator_material);
+
+ mScroller = new OverScroller(context, AnimationUtils.loadInterpolator(context,
+ android.R.interpolator.decelerate_quint));
+ mVelocityTracker = VelocityTracker.obtain();
+
+ final ViewConfiguration vc = ViewConfiguration.get(context);
+ mTouchSlop = vc.getScaledTouchSlop();
+ mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
+
+ setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+
+ /**
+ * Dynamically set the max collapsed height. Note this also updates the small collapsed
+ * height if it wasn't specified explicitly.
+ */
+ public void setMaxCollapsedHeight(int heightInPixels) {
+ if (heightInPixels == mMaxCollapsedHeight) {
+ return;
+ }
+ mMaxCollapsedHeight = heightInPixels;
+ if (!mIsMaxCollapsedHeightSmallExplicit) {
+ mMaxCollapsedHeightSmall = mMaxCollapsedHeight;
+ }
+ requestLayout();
+ }
+
+ public void setSmallCollapsed(boolean smallCollapsed) {
+ if (mSmallCollapsed != smallCollapsed) {
+ mSmallCollapsed = smallCollapsed;
+ requestLayout();
+ }
+ }
+
+ public boolean isSmallCollapsed() {
+ return mSmallCollapsed;
+ }
+
+ public boolean isCollapsed() {
+ return mCollapseOffset > 0;
+ }
+
+ public void setShowAtTop(boolean showOnTop) {
+ if (mShowAtTop != showOnTop) {
+ mShowAtTop = showOnTop;
+ requestLayout();
+ }
+ }
+
+ public boolean getShowAtTop() {
+ return mShowAtTop;
+ }
+
+ public void setCollapsed(boolean collapsed) {
+ if (!isLaidOut()) {
+ mOpenOnLayout = !collapsed;
+ } else {
+ smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0);
+ }
+ }
+
+ public void setCollapsibleHeightReserved(int heightPixels) {
+ final int oldReserved = mCollapsibleHeightReserved;
+ mCollapsibleHeightReserved = heightPixels;
+ if (oldReserved != mCollapsibleHeightReserved) {
+ requestLayout();
+ }
+
+ final int dReserved = mCollapsibleHeightReserved - oldReserved;
+ if (dReserved != 0 && mIsDragging) {
+ mLastTouchY -= dReserved;
+ }
+
+ final int oldCollapsibleHeight = mCollapsibleHeight;
+ mCollapsibleHeight = Math.min(mCollapsibleHeight, getMaxCollapsedHeight());
+
+ if (updateCollapseOffset(oldCollapsibleHeight, !isDragging())) {
+ return;
+ }
+
+ invalidate();
+ }
+
+ public void setDismissLocked(boolean locked) {
+ mDismissLocked = locked;
+ }
+
+ private boolean isMoving() {
+ return mIsDragging || !mScroller.isFinished();
+ }
+
+ private boolean isDragging() {
+ return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL;
+ }
+
+ private boolean updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed) {
+ if (oldCollapsibleHeight == mCollapsibleHeight) {
+ return false;
+ }
+
+ if (getShowAtTop()) {
+ // Keep the drawer fully open.
+ setCollapseOffset(0);
+ return false;
+ }
+
+ if (isLaidOut()) {
+ final boolean isCollapsedOld = mCollapseOffset != 0;
+ if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight
+ && mCollapseOffset == oldCollapsibleHeight)) {
+ // Stay closed even at the new height.
+ setCollapseOffset(mCollapsibleHeight);
+ } else {
+ setCollapseOffset(Math.min(mCollapseOffset, mCollapsibleHeight));
+ }
+ final boolean isCollapsedNew = mCollapseOffset != 0;
+ if (isCollapsedOld != isCollapsedNew) {
+ onCollapsedChanged(isCollapsedNew);
+ }
+ } else {
+ // Start out collapsed at first unless we restored state for otherwise
+ setCollapseOffset(mOpenOnLayout ? 0 : mCollapsibleHeight);
+ }
+ return true;
+ }
+
+ private void setCollapseOffset(float collapseOffset) {
+ if (mCollapseOffset != collapseOffset) {
+ mCollapseOffset = collapseOffset;
+ requestLayout();
+ }
+ }
+
+ private int getMaxCollapsedHeight() {
+ return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight)
+ + mCollapsibleHeightReserved;
+ }
+
+ public void setOnDismissedListener(OnDismissedListener listener) {
+ mOnDismissedListener = listener;
+ }
+
+ private boolean isDismissable() {
+ return mOnDismissedListener != null && !mDismissLocked;
+ }
+
+ public void setOnCollapsedChangedListener(OnCollapsedChangedListener listener) {
+ mOnCollapsedChangedListener = listener;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ final int action = ev.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mVelocityTracker.clear();
+ }
+
+ mVelocityTracker.addMovement(ev);
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ mInitialTouchX = x;
+ mInitialTouchY = mLastTouchY = y;
+ mOpenOnClick = isListChildUnderClipped(x, y) && mCollapseOffset > 0;
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ final float dy = y - mInitialTouchY;
+ if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null &&
+ (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
+ mActivePointerId = ev.getPointerId(0);
+ mIsDragging = true;
+ mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
+ Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP: {
+ onSecondaryPointerUp(ev);
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP: {
+ resetTouch();
+ }
+ break;
+ }
+
+ if (mIsDragging) {
+ abortAnimation();
+ }
+ return mIsDragging || mOpenOnClick;
+ }
+
+ private boolean isNestedListChildScrolled() {
+ return mNestedListChild != null
+ && mNestedListChild.getChildCount() > 0
+ && (mNestedListChild.getFirstVisiblePosition() > 0
+ || mNestedListChild.getChildAt(0).getTop() < 0);
+ }
+
+ private boolean isNestedRecyclerChildScrolled() {
+ if (mNestedRecyclerChild != null && mNestedRecyclerChild.getChildCount() > 0) {
+ final RecyclerView.ViewHolder vh =
+ mNestedRecyclerChild.findViewHolderForAdapterPosition(0);
+ return vh == null || vh.itemView.getTop() < 0;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ final int action = ev.getActionMasked();
+
+ mVelocityTracker.addMovement(ev);
+
+ boolean handled = false;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ mInitialTouchX = x;
+ mInitialTouchY = mLastTouchY = y;
+ mActivePointerId = ev.getPointerId(0);
+ final boolean hitView = findChildUnder(mInitialTouchX, mInitialTouchY) != null;
+ handled = isDismissable() || mCollapsibleHeight > 0;
+ mIsDragging = hitView && handled;
+ abortAnimation();
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE: {
+ int index = ev.findPointerIndex(mActivePointerId);
+ if (index < 0) {
+ Log.e(TAG, "Bad pointer id " + mActivePointerId + ", resetting");
+ index = 0;
+ mActivePointerId = ev.getPointerId(0);
+ mInitialTouchX = ev.getX();
+ mInitialTouchY = mLastTouchY = ev.getY();
+ }
+ final float x = ev.getX(index);
+ final float y = ev.getY(index);
+ if (!mIsDragging) {
+ final float dy = y - mInitialTouchY;
+ if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null) {
+ handled = mIsDragging = true;
+ mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
+ Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
+ }
+ }
+ if (mIsDragging) {
+ final float dy = y - mLastTouchY;
+ if (dy > 0 && isNestedListChildScrolled()) {
+ mNestedListChild.smoothScrollBy((int) -dy, 0);
+ } else if (dy > 0 && isNestedRecyclerChildScrolled()) {
+ mNestedRecyclerChild.scrollBy(0, (int) -dy);
+ } else {
+ performDrag(dy);
+ }
+ }
+ mLastTouchY = y;
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ final int pointerIndex = ev.getActionIndex();
+ mActivePointerId = ev.getPointerId(pointerIndex);
+ mInitialTouchX = ev.getX(pointerIndex);
+ mInitialTouchY = mLastTouchY = ev.getY(pointerIndex);
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP: {
+ onSecondaryPointerUp(ev);
+ }
+ break;
+
+ case MotionEvent.ACTION_UP: {
+ final boolean wasDragging = mIsDragging;
+ mIsDragging = false;
+ if (!wasDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null &&
+ findChildUnder(ev.getX(), ev.getY()) == null) {
+ if (isDismissable()) {
+ dispatchOnDismissed();
+ resetTouch();
+ return true;
+ }
+ }
+ if (mOpenOnClick && Math.abs(ev.getX() - mInitialTouchX) < mTouchSlop &&
+ Math.abs(ev.getY() - mInitialTouchY) < mTouchSlop) {
+ smoothScrollTo(0, 0);
+ return true;
+ }
+ mVelocityTracker.computeCurrentVelocity(1000);
+ final float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
+ if (Math.abs(yvel) > mMinFlingVelocity) {
+ if (getShowAtTop()) {
+ if (isDismissable() && yvel < 0) {
+ abortAnimation();
+ dismiss();
+ } else {
+ smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
+ }
+ } else {
+ if (isDismissable()
+ && yvel > 0 && mCollapseOffset > mCollapsibleHeight) {
+ smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, yvel);
+ mDismissOnScrollerFinished = true;
+ } else {
+ scrollNestedScrollableChildBackToTop();
+ smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
+ }
+ }
+ }else {
+ smoothScrollTo(
+ mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
+ }
+ resetTouch();
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL: {
+ if (mIsDragging) {
+ smoothScrollTo(
+ mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
+ }
+ resetTouch();
+ return true;
+ }
+ }
+
+ return handled;
+ }
+
+ /**
+ * Scroll nested scrollable child back to top if it has been scrolled.
+ */
+ public void scrollNestedScrollableChildBackToTop() {
+ if (isNestedListChildScrolled()) {
+ mNestedListChild.smoothScrollToPosition(0);
+ } else if (isNestedRecyclerChildScrolled()) {
+ mNestedRecyclerChild.smoothScrollToPosition(0);
+ }
+ }
+
+ private void onSecondaryPointerUp(MotionEvent ev) {
+ final int pointerIndex = ev.getActionIndex();
+ final int pointerId = ev.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mInitialTouchX = ev.getX(newPointerIndex);
+ mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex);
+ mActivePointerId = ev.getPointerId(newPointerIndex);
+ }
+ }
+
+ private void resetTouch() {
+ mActivePointerId = MotionEvent.INVALID_POINTER_ID;
+ mIsDragging = false;
+ mOpenOnClick = false;
+ mInitialTouchX = mInitialTouchY = mLastTouchY = 0;
+ mVelocityTracker.clear();
+ }
+
+ private void dismiss() {
+ mRunOnDismissedListener = new RunOnDismissedListener();
+ post(mRunOnDismissedListener);
+ }
+
+ @Override
+ public void computeScroll() {
+ super.computeScroll();
+ if (mScroller.computeScrollOffset()) {
+ final boolean keepGoing = !mScroller.isFinished();
+ performDrag(mScroller.getCurrY() - mCollapseOffset);
+ if (keepGoing) {
+ postInvalidateOnAnimation();
+ } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) {
+ dismiss();
+ }
+ }
+ }
+
+ private void abortAnimation() {
+ mScroller.abortAnimation();
+ mRunOnDismissedListener = null;
+ mDismissOnScrollerFinished = false;
+ }
+
+ private float performDrag(float dy) {
+ if (getShowAtTop()) {
+ return 0;
+ }
+
+ final float newPos = Math.max(0, Math.min(mCollapseOffset + dy,
+ mCollapsibleHeight + mUncollapsibleHeight));
+ if (newPos != mCollapseOffset) {
+ dy = newPos - mCollapseOffset;
+
+ mDragRemainder += dy - (int) dy;
+ if (mDragRemainder >= 1.0f) {
+ mDragRemainder -= 1.0f;
+ dy += 1.0f;
+ } else if (mDragRemainder <= -1.0f) {
+ mDragRemainder += 1.0f;
+ dy -= 1.0f;
+ }
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!lp.ignoreOffset) {
+ child.offsetTopAndBottom((int) dy);
+ }
+ }
+ final boolean isCollapsedOld = mCollapseOffset != 0;
+ mCollapseOffset = newPos;
+ mTopOffset += dy;
+ final boolean isCollapsedNew = newPos != 0;
+ if (isCollapsedOld != isCollapsedNew) {
+ onCollapsedChanged(isCollapsedNew);
+ getMetricsLogger().write(
+ new LogMaker(MetricsEvent.ACTION_SHARESHEET_COLLAPSED_CHANGED)
+ .setSubtype(isCollapsedNew ? 1 : 0));
+ }
+ onScrollChanged(0, (int) newPos, 0, (int) (newPos - dy));
+ postInvalidateOnAnimation();
+ return dy;
+ }
+ return 0;
+ }
+
+ private void onCollapsedChanged(boolean isCollapsed) {
+ notifyViewAccessibilityStateChangedIfNeeded(
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+
+ if (mScrollIndicatorDrawable != null) {
+ setWillNotDraw(!isCollapsed);
+ }
+
+ if (mOnCollapsedChangedListener != null) {
+ mOnCollapsedChangedListener.onCollapsedChanged(isCollapsed);
+ }
+ }
+
+ void dispatchOnDismissed() {
+ if (mOnDismissedListener != null) {
+ mOnDismissedListener.onDismissed();
+ }
+ if (mRunOnDismissedListener != null) {
+ removeCallbacks(mRunOnDismissedListener);
+ mRunOnDismissedListener = null;
+ }
+ }
+
+ private void smoothScrollTo(int yOffset, float velocity) {
+ abortAnimation();
+ final int sy = (int) mCollapseOffset;
+ int dy = yOffset - sy;
+ if (dy == 0) {
+ return;
+ }
+
+ final int height = getHeight();
+ final int halfHeight = height / 2;
+ final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / height);
+ final float distance = halfHeight + halfHeight *
+ distanceInfluenceForSnapDuration(distanceRatio);
+
+ int duration = 0;
+ velocity = Math.abs(velocity);
+ if (velocity > 0) {
+ duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
+ } else {
+ final float pageDelta = (float) Math.abs(dy) / height;
+ duration = (int) ((pageDelta + 1) * 100);
+ }
+ duration = Math.min(duration, 300);
+
+ mScroller.startScroll(0, sy, 0, dy, duration);
+ postInvalidateOnAnimation();
+ }
+
+ private float distanceInfluenceForSnapDuration(float f) {
+ f -= 0.5f; // center the values about 0.
+ f *= 0.3f * Math.PI / 2.0f;
+ return (float) Math.sin(f);
+ }
+
+ /**
+ * Note: this method doesn't take Z into account for overlapping views
+ * since it is only used in contexts where this doesn't affect the outcome.
+ */
+ private View findChildUnder(float x, float y) {
+ return findChildUnder(this, x, y);
+ }
+
+ private static View findChildUnder(ViewGroup parent, float x, float y) {
+ final int childCount = parent.getChildCount();
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View child = parent.getChildAt(i);
+ if (isChildUnder(child, x, y)) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ private View findListChildUnder(float x, float y) {
+ View v = findChildUnder(x, y);
+ while (v != null) {
+ x -= v.getX();
+ y -= v.getY();
+ if (v instanceof AbsListView) {
+ // One more after this.
+ return findChildUnder((ViewGroup) v, x, y);
+ }
+ v = v instanceof ViewGroup ? findChildUnder((ViewGroup) v, x, y) : null;
+ }
+ return v;
+ }
+
+ /**
+ * This only checks clipping along the bottom edge.
+ */
+ private boolean isListChildUnderClipped(float x, float y) {
+ final View listChild = findListChildUnder(x, y);
+ return listChild != null && isDescendantClipped(listChild);
+ }
+
+ private boolean isDescendantClipped(View child) {
+ mTempRect.set(0, 0, child.getWidth(), child.getHeight());
+ offsetDescendantRectToMyCoords(child, mTempRect);
+ View directChild;
+ if (child.getParent() == this) {
+ directChild = child;
+ } else {
+ View v = child;
+ ViewParent p = child.getParent();
+ while (p != this) {
+ v = (View) p;
+ p = v.getParent();
+ }
+ directChild = v;
+ }
+
+ // ResolverDrawerLayout lays out vertically in child order;
+ // the next view and forward is what to check against.
+ int clipEdge = getHeight() - getPaddingBottom();
+ final int childCount = getChildCount();
+ for (int i = indexOfChild(directChild) + 1; i < childCount; i++) {
+ final View nextChild = getChildAt(i);
+ if (nextChild.getVisibility() == GONE) {
+ continue;
+ }
+ clipEdge = Math.min(clipEdge, nextChild.getTop());
+ }
+ return mTempRect.bottom > clipEdge;
+ }
+
+ private static boolean isChildUnder(View child, float x, float y) {
+ final float left = child.getX();
+ final float top = child.getY();
+ final float right = left + child.getWidth();
+ final float bottom = top + child.getHeight();
+ return x >= left && y >= top && x < right && y < bottom;
+ }
+
+ @Override
+ public void requestChildFocus(View child, View focused) {
+ super.requestChildFocus(child, focused);
+ if (!isInTouchMode() && isDescendantClipped(focused)) {
+ smoothScrollTo(0, 0);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getViewTreeObserver().addOnTouchModeChangeListener(mTouchModeChangeListener);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ getViewTreeObserver().removeOnTouchModeChangeListener(mTouchModeChangeListener);
+ abortAnimation();
+ }
+
+ @Override
+ public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
+ if ((nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0) {
+ if (target instanceof AbsListView) {
+ mNestedListChild = (AbsListView) target;
+ }
+ if (target instanceof RecyclerView) {
+ mNestedRecyclerChild = (RecyclerView) target;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onNestedScrollAccepted(View child, View target, int axes) {
+ super.onNestedScrollAccepted(child, target, axes);
+ }
+
+ @Override
+ public void onStopNestedScroll(View child) {
+ super.onStopNestedScroll(child);
+ if (mScroller.isFinished()) {
+ smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
+ }
+ }
+
+ @Override
+ public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
+ int dxUnconsumed, int dyUnconsumed) {
+ if (dyUnconsumed < 0) {
+ performDrag(-dyUnconsumed);
+ }
+ }
+
+ @Override
+ public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
+ if (dy > 0) {
+ consumed[1] = (int) -performDrag(-dy);
+ }
+ }
+
+ @Override
+ public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+ if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) {
+ smoothScrollTo(0, velocityY);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+ if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) {
+ if (getShowAtTop()) {
+ if (isDismissable() && velocityY > 0) {
+ abortAnimation();
+ dismiss();
+ } else {
+ smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY);
+ }
+ } else {
+ if (isDismissable()
+ && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) {
+ smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, velocityY);
+ mDismissOnScrollerFinished = true;
+ } else {
+ smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY);
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private boolean performAccessibilityActionCommon(int action) {
+ switch (action) {
+ case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+ case AccessibilityNodeInfo.ACTION_EXPAND:
+ case com.android.internal.R.id.accessibilityActionScrollDown:
+ if (mCollapseOffset != 0) {
+ smoothScrollTo(0, 0);
+ return true;
+ }
+ break;
+ case AccessibilityNodeInfo.ACTION_COLLAPSE:
+ if (mCollapseOffset < mCollapsibleHeight) {
+ smoothScrollTo(mCollapsibleHeight, 0);
+ return true;
+ }
+ break;
+ case AccessibilityNodeInfo.ACTION_DISMISS:
+ if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight)
+ && isDismissable()) {
+ smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, 0);
+ mDismissOnScrollerFinished = true;
+ return true;
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
+ if (super.onNestedPrePerformAccessibilityAction(target, action, args)) {
+ return true;
+ }
+
+ return performAccessibilityActionCommon(action);
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ // Since we support scrolling, make this ViewGroup look like a
+ // ScrollView. This is kind of a hack until we have support for
+ // specifying auto-scroll behavior.
+ return android.widget.ScrollView.class.getName();
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfoInternal(info);
+
+ if (isEnabled()) {
+ if (mCollapseOffset != 0) {
+ info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
+ info.addAction(AccessibilityAction.ACTION_EXPAND);
+ info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN);
+ info.setScrollable(true);
+ }
+ if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight)
+ && ((mCollapseOffset < mCollapsibleHeight) || isDismissable())) {
+ info.addAction(AccessibilityAction.ACTION_SCROLL_UP);
+ info.setScrollable(true);
+ }
+ if (mCollapseOffset < mCollapsibleHeight) {
+ info.addAction(AccessibilityAction.ACTION_COLLAPSE);
+ }
+ if (mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight && isDismissable()) {
+ info.addAction(AccessibilityAction.ACTION_DISMISS);
+ }
+ }
+
+ // This view should never get accessibility focus, but it's interactive
+ // via nested scrolling, so we can't hide it completely.
+ info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
+ }
+
+ @Override
+ public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+ if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) {
+ // This view should never get accessibility focus.
+ return false;
+ }
+
+ if (super.performAccessibilityActionInternal(action, arguments)) {
+ return true;
+ }
+
+ return performAccessibilityActionCommon(action);
+ }
+
+ @Override
+ public void onDrawForeground(Canvas canvas) {
+ if (mScrollIndicatorDrawable != null) {
+ mScrollIndicatorDrawable.draw(canvas);
+ }
+
+ super.onDrawForeground(canvas);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec);
+ int widthSize = sourceWidth;
+ final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ // Single-use layout; just ignore the mode and use available space.
+ // Clamp to maxWidth.
+ if (mMaxWidth >= 0) {
+ widthSize = Math.min(widthSize, mMaxWidth + getPaddingLeft() + getPaddingRight());
+ }
+
+ final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
+ final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
+
+ // Currently we allot more height than is really needed so that the entirety of the
+ // sheet may be pulled up.
+ // TODO: Restrict the height here to be the right value.
+ int heightUsed = 0;
+
+ // Measure always-show children first.
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.alwaysShow && child.getVisibility() != GONE) {
+ if (lp.maxHeight != -1) {
+ final int remainingHeight = heightSize - heightUsed;
+ measureChildWithMargins(child, widthSpec, 0,
+ MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
+ lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
+ } else {
+ measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
+ }
+ heightUsed += child.getMeasuredHeight();
+ }
+ }
+
+ mAlwaysShowHeight = heightUsed;
+
+ // And now the rest.
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!lp.alwaysShow && child.getVisibility() != GONE) {
+ if (lp.maxHeight != -1) {
+ final int remainingHeight = heightSize - heightUsed;
+ measureChildWithMargins(child, widthSpec, 0,
+ MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
+ lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
+ } else {
+ measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
+ }
+ heightUsed += child.getMeasuredHeight();
+ }
+ }
+
+ final int oldCollapsibleHeight = mCollapsibleHeight;
+ mCollapsibleHeight = Math.max(0,
+ heightUsed - mAlwaysShowHeight - getMaxCollapsedHeight());
+ mUncollapsibleHeight = heightUsed - mCollapsibleHeight;
+
+ updateCollapseOffset(oldCollapsibleHeight, !isDragging());
+
+ if (getShowAtTop()) {
+ mTopOffset = 0;
+ } else {
+ mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset;
+ }
+
+ setMeasuredDimension(sourceWidth, heightSize);
+ }
+
+ /**
+ * @return The space reserved by views with 'alwaysShow=true'
+ */
+ public int getAlwaysShowHeight() {
+ return mAlwaysShowHeight;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int width = getWidth();
+
+ View indicatorHost = null;
+
+ int ypos = mTopOffset;
+ final int leftEdge = getPaddingLeft();
+ final int rightEdge = width - getPaddingRight();
+ final int widthAvailable = rightEdge - leftEdge;
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.hasNestedScrollIndicator) {
+ indicatorHost = child;
+ }
+
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ int top = ypos + lp.topMargin;
+ if (lp.ignoreOffset) {
+ top -= mCollapseOffset;
+ }
+ final int bottom = top + child.getMeasuredHeight();
+
+ final int childWidth = child.getMeasuredWidth();
+ final int left = leftEdge + (widthAvailable - childWidth) / 2;
+ final int right = left + childWidth;
+
+ child.layout(left, top, right, bottom);
+
+ ypos = bottom + lp.bottomMargin;
+ }
+
+ if (mScrollIndicatorDrawable != null) {
+ if (indicatorHost != null) {
+ final int left = indicatorHost.getLeft();
+ final int right = indicatorHost.getRight();
+ final int bottom = indicatorHost.getTop();
+ final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight();
+ mScrollIndicatorDrawable.setBounds(left, top, right, bottom);
+ setWillNotDraw(!isCollapsed());
+ } else {
+ mScrollIndicatorDrawable = null;
+ setWillNotDraw(true);
+ }
+ }
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ if (p instanceof LayoutParams) {
+ return new LayoutParams((LayoutParams) p);
+ } else if (p instanceof MarginLayoutParams) {
+ return new LayoutParams((MarginLayoutParams) p);
+ }
+ return new LayoutParams(p);
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final SavedState ss = new SavedState(super.onSaveInstanceState());
+ ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0;
+ ss.mCollapsibleHeightReserved = mCollapsibleHeightReserved;
+ return ss;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ final SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ mOpenOnLayout = ss.open;
+ mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved;
+ }
+
+ public static class LayoutParams extends MarginLayoutParams {
+ public boolean alwaysShow;
+ public boolean ignoreOffset;
+ public boolean hasNestedScrollIndicator;
+ public int maxHeight;
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+
+ final TypedArray a = c.obtainStyledAttributes(attrs,
+ R.styleable.ResolverDrawerLayout_LayoutParams);
+ alwaysShow = a.getBoolean(
+ R.styleable.ResolverDrawerLayout_LayoutParams_layout_alwaysShow,
+ false);
+ ignoreOffset = a.getBoolean(
+ R.styleable.ResolverDrawerLayout_LayoutParams_layout_ignoreOffset,
+ false);
+ hasNestedScrollIndicator = a.getBoolean(
+ R.styleable.ResolverDrawerLayout_LayoutParams_layout_hasNestedScrollIndicator,
+ false);
+ maxHeight = a.getDimensionPixelSize(
+ R.styleable.ResolverDrawerLayout_LayoutParams_layout_maxHeight, -1);
+ a.recycle();
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ public LayoutParams(LayoutParams source) {
+ super(source);
+ this.alwaysShow = source.alwaysShow;
+ this.ignoreOffset = source.ignoreOffset;
+ this.hasNestedScrollIndicator = source.hasNestedScrollIndicator;
+ this.maxHeight = source.maxHeight;
+ }
+
+ public LayoutParams(MarginLayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+ }
+
+ static class SavedState extends BaseSavedState {
+ boolean open;
+ private int mCollapsibleHeightReserved;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ open = in.readInt() != 0;
+ mCollapsibleHeightReserved = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(open ? 1 : 0);
+ out.writeInt(mCollapsibleHeightReserved);
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+
+ /**
+ * Listener for sheet dismissed events.
+ */
+ public interface OnDismissedListener {
+ /**
+ * Callback when the sheet is dismissed by the user.
+ */
+ void onDismissed();
+ }
+
+ /**
+ * Listener for sheet collapsed / expanded events.
+ */
+ public interface OnCollapsedChangedListener {
+ /**
+ * Callback when the sheet is either fully expanded or collapsed.
+ * @param isCollapsed true when collapsed, false when expanded.
+ */
+ void onCollapsedChanged(boolean isCollapsed);
+ }
+
+ private class RunOnDismissedListener implements Runnable {
+ @Override
+ public void run() {
+ dispatchOnDismissed();
+ }
+ }
+
+ private MetricsLogger getMetricsLogger() {
+ if (mMetricsLogger == null) {
+ mMetricsLogger = new MetricsLogger();
+ }
+ return mMetricsLogger;
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
index 7a590584..9ac815f6 100644
--- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
+++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
@@ -86,6 +86,8 @@ import android.view.View;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.matcher.BoundedDiagnosingMatcher;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
@@ -96,8 +98,6 @@ import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.FrameworkStatsLog;
-import com.android.internal.widget.GridLayoutManager;
-import com.android.internal.widget.RecyclerView;
import org.hamcrest.Description;
import org.hamcrest.Matcher;