summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml48
-rw-r--r--AndroidManifestLib.xml2
-rw-r--r--compose/AndroidManifest.xml2
-rw-r--r--flags.aconfig7
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_bottom_section_background.xml24
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_middle_section_background.xml (renamed from res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_background.xml)2
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_top_section_background.xml24
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/sort_widget_background.xml1
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout-w900dp/column_headers.xml1
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/directory_app_bar.xml4
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/directory_header.xml3
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/drawer_layout.xml6
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/fixed_layout.xml79
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/nav_rail_layout.xml81
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values/dimens.xml3
-rw-r--r--src/com/android/documentsui/AbstractActionHandler.java2
-rw-r--r--src/com/android/documentsui/ActionHandler.java2
-rw-r--r--src/com/android/documentsui/BaseActivity.java15
-rw-r--r--src/com/android/documentsui/dirlist/DirectoryFragment.java2
-rw-r--r--src/com/android/documentsui/files/ActionHandler.java20
-rw-r--r--src/com/android/documentsui/files/FilesActivity.java2
-rw-r--r--src/com/android/documentsui/picker/TrampolineActivity.kt170
-rw-r--r--src/com/android/documentsui/ui/DocumentDebugInfo.java59
-rw-r--r--tests/AndroidManifest.xml2
-rw-r--r--tests/common/com/android/documentsui/testing/TestModel.java18
-rw-r--r--tests/functional/com/android/documentsui/SortDocumentUiTest.java8
-rw-r--r--tests/functional/com/android/documentsui/TrampolineActivityTest.kt218
-rw-r--r--tests/unit/com/android/documentsui/files/ActionHandlerTest.java23
-rw-r--r--tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java2
-rw-r--r--tests/unit/com/android/documentsui/loaders/BaseLoaderTest.kt36
-rw-r--r--tests/unit/com/android/documentsui/loaders/FolderLoaderTest.kt34
-rw-r--r--tests/unit/com/android/documentsui/loaders/SearchLoaderTest.kt42
-rw-r--r--tests/unit/com/android/documentsui/picker/ActionHandlerTest.java3
33 files changed, 779 insertions, 166 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 6b4bed88f..1fa1ac3b6 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -19,7 +19,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.documentsui">
- <uses-sdk android:minSdkVersion="29"/>
+ <uses-sdk android:minSdkVersion="30"/>
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
<uses-permission android:name="android.permission.REMOVE_TASKS" />
@@ -60,29 +60,67 @@
android:value="AEdPqrEAAAAInBA8ued0O_ZyYUsVhwinUF-x50NIe9K0GzBW4A" />
<activity
+ android:name=".picker.TrampolineActivity"
+ android:exported="true"
+ android:theme="@android:style/Theme.NoDisplay"
+ android:featureFlag="com.android.documentsui.flags.redirect_get_content"
+ android:visibleToInstantApps="true">
+ <intent-filter android:priority="120">
+ <action android:name="android.intent.action.OPEN_DOCUMENT" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.OPENABLE" />
+ <data android:mimeType="*/*" />
+ </intent-filter>
+ <intent-filter android:priority="120">
+ <action android:name="android.intent.action.CREATE_DOCUMENT" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.OPENABLE" />
+ <data android:mimeType="*/*" />
+ </intent-filter>
+ <intent-filter android:priority="120">
+ <action android:name="android.intent.action.GET_CONTENT" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.OPENABLE" />
+ <data android:mimeType="*/*" />
+ </intent-filter>
+ <intent-filter android:priority="120">
+ <action android:name="android.intent.action.OPEN_DOCUMENT_TREE" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity
android:name=".picker.PickActivity"
android:exported="true"
android:theme="@style/LauncherTheme"
android:visibleToInstantApps="true">
- <intent-filter android:priority="100">
+ <intent-filter
+ android:featureFlag="!com.android.documentsui.flags.redirect_get_content"
+ android:priority="100">
<action android:name="android.intent.action.OPEN_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="*/*" />
</intent-filter>
- <intent-filter android:priority="100">
+ <intent-filter
+ android:featureFlag="!com.android.documentsui.flags.redirect_get_content"
+ android:priority="100">
<action android:name="android.intent.action.CREATE_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="*/*" />
</intent-filter>
- <intent-filter android:priority="100">
+ <intent-filter
+ android:featureFlag="!com.android.documentsui.flags.redirect_get_content"
+ android:priority="100">
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="*/*" />
</intent-filter>
- <intent-filter android:priority="100">
+ <intent-filter
+ android:featureFlag="!com.android.documentsui.flags.redirect_get_content"
+ android:priority="100">
<action android:name="android.intent.action.OPEN_DOCUMENT_TREE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
diff --git a/AndroidManifestLib.xml b/AndroidManifestLib.xml
index 993d4409a..b7e54192b 100644
--- a/AndroidManifestLib.xml
+++ b/AndroidManifestLib.xml
@@ -18,7 +18,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.documentsui">
- <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
+ <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" />
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
<uses-permission android:name="android.permission.REMOVE_TASKS" />
diff --git a/compose/AndroidManifest.xml b/compose/AndroidManifest.xml
index dbed77310..9b943e086 100644
--- a/compose/AndroidManifest.xml
+++ b/compose/AndroidManifest.xml
@@ -19,7 +19,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.documentsui.compose">
- <uses-sdk android:minSdkVersion="29"/>
+ <uses-sdk android:minSdkVersion="30"/>
<!-- Permissions copied from com.android.documentsui AndroidManifest.xml -->
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
diff --git a/flags.aconfig b/flags.aconfig
index 6a1742a27..c4deba628 100644
--- a/flags.aconfig
+++ b/flags.aconfig
@@ -58,3 +58,10 @@ flag {
description: "Redirects GET_CONTENT requests to Photopicker when appropriate"
bug: "377771195"
}
+
+flag {
+ name: "use_peek_preview"
+ namespace: "documentsui"
+ description: "Enables the Peek previewing capability as a substitute for the Inspector."
+ bug: "373242058"
+}
diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_bottom_section_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_bottom_section_background.xml
new file mode 100644
index 000000000..04f425e16
--- /dev/null
+++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_bottom_section_background.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="?attr/colorSurfaceBright" />
+ <corners
+ android:topLeftRadius="@dimen/main_container_corner_radius_small"
+ android:topRightRadius="@dimen/main_container_corner_radius_small"
+ android:bottomLeftRadius="@dimen/main_container_corner_radius_large"
+ android:bottomRightRadius="@dimen/main_container_corner_radius_large" />
+</shape> \ No newline at end of file
diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_middle_section_background.xml
index 151a7ba77..8b0b42bee 100644
--- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_background.xml
+++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_middle_section_background.xml
@@ -16,5 +16,5 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?attr/colorSurfaceBright" />
- <corners android:radius="16dp" />
+ <corners android:radius="@dimen/main_container_corner_radius_small" />
</shape> \ No newline at end of file
diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_top_section_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_top_section_background.xml
new file mode 100644
index 000000000..b35ed4060
--- /dev/null
+++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_top_section_background.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="?attr/colorSurfaceBright" />
+ <corners
+ android:topLeftRadius="@dimen/main_container_corner_radius_large"
+ android:topRightRadius="@dimen/main_container_corner_radius_large"
+ android:bottomLeftRadius="@dimen/main_container_corner_radius_small"
+ android:bottomRightRadius="@dimen/main_container_corner_radius_small" />
+</shape> \ No newline at end of file
diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/sort_widget_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/sort_widget_background.xml
index 212dab765..5a9022035 100644
--- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/sort_widget_background.xml
+++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/sort_widget_background.xml
@@ -15,6 +15,7 @@
limitations under the License.
-->
+<!-- TODO(b/379776735): remove this file after M3 uplift -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:top="-1dp"
diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout-w900dp/column_headers.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout-w900dp/column_headers.xml
index 57fb74751..e0b3ff430 100644
--- a/res/flag(com.android.documentsui.flags.use_material3)/layout-w900dp/column_headers.xml
+++ b/res/flag(com.android.documentsui.flags.use_material3)/layout-w900dp/column_headers.xml
@@ -21,7 +21,6 @@
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="@dimen/doc_header_height"
- android:background="@drawable/sort_widget_background"
android:visibility="gone">
<LinearLayout
diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/directory_app_bar.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/directory_app_bar.xml
index 95b322454..54a6a7cb2 100644
--- a/res/flag(com.android.documentsui.flags.use_material3)/layout/directory_app_bar.xml
+++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/directory_app_bar.xml
@@ -14,6 +14,7 @@
limitations under the License.
-->
+<!-- This is only used in DrawerLayout (compact screen) right now. -->
<com.google.android.material.appbar.AppBarLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
@@ -35,6 +36,9 @@
<include layout="@layout/directory_header" />
+ <!-- column headers are empty on small screens, in portrait or in grid mode. -->
+ <include layout="@layout/column_headers"/>
+
</androidx.core.widget.NestedScrollView>
<com.google.android.material.appbar.MaterialToolbar
diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/directory_header.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/directory_header.xml
index 7623403f6..171c5882a 100644
--- a/res/flag(com.android.documentsui.flags.use_material3)/layout/directory_header.xml
+++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/directory_header.xml
@@ -52,7 +52,4 @@
<!-- used for apps row. -->
<include layout="@layout/apps_row"/>
- <!-- column headers are empty on small screens, in portrait or in grid mode. -->
- <include layout="@layout/column_headers"/>
-
</LinearLayout> \ No newline at end of file
diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/drawer_layout.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/drawer_layout.xml
index 8499c0112..e86e0ec5a 100644
--- a/res/flag(com.android.documentsui.flags.use_material3)/layout/drawer_layout.xml
+++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/drawer_layout.xml
@@ -28,6 +28,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
+ <!-- Main container -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -35,6 +36,7 @@
android:paddingTop="@dimen/main_container_padding_top"
android:background="?attr/colorSurfaceBright">
+ <!-- Main list area (file list/grid or search results), full height -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -62,11 +64,13 @@
android:layout_height="match_parent"/>
</FrameLayout>
+ <!-- Footer of right hand side: Breadcrumbs and Picker footer. -->
<com.android.documentsui.HorizontalBreadcrumb
android:id="@+id/horizontal_breadcrumb"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
+ android:background="?attr/colorSurfaceBright"
/>
<androidx.coordinatorlayout.widget.CoordinatorLayout
@@ -77,10 +81,12 @@
android:background="?android:attr/colorBackgroundFloating"
/>
+ <!-- Top section: toolbar, search chips, profile tab -->
<include layout="@layout/directory_app_bar"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
+ <!-- Drawer section -->
<LinearLayout
android:id="@+id/drawer_roots"
android:layout_width="256dp"
diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/fixed_layout.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/fixed_layout.xml
index 0b03572ac..feaa34e6d 100644
--- a/res/flag(com.android.documentsui.flags.use_material3)/layout/fixed_layout.xml
+++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/fixed_layout.xml
@@ -26,33 +26,33 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:baselineAligned="false"
android:background="?attr/colorSurfaceContainer"
- android:orientation="vertical">
-
+ android:paddingTop="@dimen/layout_padding_top"
+ android:paddingBottom="@dimen/layout_padding_bottom"
+ android:paddingEnd="@dimen/layout_padding_end">
+
+ <!-- Navigation: left hand side. -->
+ <FrameLayout
+ android:id="@+id/container_roots"
+ android:layout_width="256dp"
+ android:layout_height="match_parent"
+ />
+
+ <!-- Main container for the right hand side. -->
<LinearLayout
android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:orientation="horizontal"
- android:baselineAligned="false"
- android:paddingTop="@dimen/layout_padding_top"
- android:paddingBottom="@dimen/layout_padding_bottom"
- android:paddingEnd="@dimen/layout_padding_end">
-
- <!-- Navigation: left hand side. -->
- <FrameLayout
- android:id="@+id/container_roots"
- android:layout_width="256dp"
- android:layout_height="match_parent"
- />
-
- <!-- Main container for the right hand side. -->
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <!-- Top section: toolbar, search chips, profile tab -->
<LinearLayout
android:layout_width="match_parent"
- android:layout_height="match_parent"
+ android:layout_height="wrap_content"
android:orientation="vertical"
- android:background="@drawable/main_container_background"
- android:paddingTop="@dimen/main_container_padding_top">
+ android:paddingTop="@dimen/main_container_padding_top"
+ android:background="@drawable/main_container_top_section_background">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
@@ -73,7 +73,19 @@
<include layout="@layout/directory_header" />
- <!-- Main list area (file list/grid or search results). -->
+ </LinearLayout>
+
+ <!-- Main list area (file list/grid or search results). -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:layout_marginTop="@dimen/main_container_section_gap"
+ android:background="@drawable/main_container_middle_section_background">
+
+ <include layout="@layout/column_headers"/>
+
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
@@ -92,22 +104,29 @@
android:layout_height="match_parent" />
</FrameLayout>
+ </LinearLayout>
+
+ <!-- Footer of right hand side: Breadcrumbs and Picker footer. -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/main_container_section_gap"
+ android:background="@drawable/main_container_bottom_section_background">
- <!-- Footer of right hand side: Breadcrumbs and Picker footer. -->
<com.android.documentsui.HorizontalBreadcrumb
android:id="@+id/horizontal_breadcrumb"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
- <androidx.coordinatorlayout.widget.CoordinatorLayout
- android:id="@+id/container_save"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="?android:attr/colorBackgroundFloating"
- android:elevation="8dp" />
-
</LinearLayout>
+ <androidx.coordinatorlayout.widget.CoordinatorLayout
+ android:id="@+id/container_save"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/colorBackgroundFloating"
+ android:elevation="8dp" />
+
</LinearLayout>
</LinearLayout>
diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/nav_rail_layout.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/nav_rail_layout.xml
index 5d753f336..618aa7f14 100644
--- a/res/flag(com.android.documentsui.flags.use_material3)/layout/nav_rail_layout.xml
+++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/nav_rail_layout.xml
@@ -49,54 +49,81 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical"
- android:background="@drawable/main_container_background"
- android:paddingTop="@dimen/main_container_padding_top">
+ android:orientation="vertical">
- <com.google.android.material.appbar.MaterialToolbar
- android:id="@+id/toolbar"
+ <!-- Top section: toolbar, search chips, profile tab -->
+ <LinearLayout
android:layout_width="match_parent"
- android:layout_height="?attr/actionBarSize"
- android:layout_marginTop="@dimen/action_bar_margin"
- android:touchscreenBlocksFocus="false">
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingTop="@dimen/main_container_padding_top"
+ android:background="@drawable/main_container_top_section_background">
- <TextView
- android:id="@+id/searchbar_title"
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/toolbar"
android:layout_width="match_parent"
- android:layout_height="?android:attr/actionBarSize"
- android:gravity="center_vertical"
- android:text="@string/search_bar_hint"
- android:textAppearance="@style/SearchBarTitle" />
+ android:layout_height="?attr/actionBarSize"
+ android:layout_marginTop="@dimen/action_bar_margin"
+ android:touchscreenBlocksFocus="false">
+
+ <TextView
+ android:id="@+id/searchbar_title"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/actionBarSize"
+ android:gravity="center_vertical"
+ android:text="@string/search_bar_hint"
+ android:textAppearance="@style/SearchBarTitle" />
+
+ </com.google.android.material.appbar.MaterialToolbar>
- </com.google.android.material.appbar.MaterialToolbar>
+ <include layout="@layout/directory_header" />
- <include layout="@layout/directory_header" />
+ </LinearLayout>
<!-- Main list area (file list/grid or search results). -->
- <FrameLayout
+ <LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
- android:layout_weight="1">
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:layout_marginTop="@dimen/main_container_section_gap"
+ android:background="@drawable/main_container_middle_section_background">
- <FrameLayout
- android:id="@+id/container_directory"
- android:clipToPadding="false"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
+ <include layout="@layout/column_headers"/>
<FrameLayout
- android:id="@+id/container_search_fragment"
- android:clipToPadding="false"
android:layout_width="match_parent"
- android:layout_height="match_parent" />
+ android:layout_height="0dp"
+ android:layout_weight="1">
+
+ <FrameLayout
+ android:id="@+id/container_directory"
+ android:clipToPadding="false"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
- </FrameLayout>
+ <FrameLayout
+ android:id="@+id/container_search_fragment"
+ android:clipToPadding="false"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ </FrameLayout>
+
+ </LinearLayout>
<!-- Footer of right hand side: Breadcrumbs and Picker footer. -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/main_container_section_gap"
+ android:background="@drawable/main_container_bottom_section_background">
+
<com.android.documentsui.HorizontalBreadcrumb
android:id="@+id/horizontal_breadcrumb"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
+ </LinearLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/container_save"
diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values/dimens.xml b/res/flag(com.android.documentsui.flags.use_material3)/values/dimens.xml
index 7ca1ec25a..c6a7ba8fd 100644
--- a/res/flag(com.android.documentsui.flags.use_material3)/values/dimens.xml
+++ b/res/flag(com.android.documentsui.flags.use_material3)/values/dimens.xml
@@ -130,6 +130,9 @@
<dimen name="main_container_padding_start">@dimen/space_small_4</dimen>
<dimen name="main_container_padding_end">@dimen/space_small_4</dimen>
<dimen name="main_container_padding_top">0dp</dimen>
+ <dimen name="main_container_section_gap">2dp</dimen>
+ <dimen name="main_container_corner_radius_large">16dp</dimen>
+ <dimen name="main_container_corner_radius_small">4dp</dimen>
<dimen name="layout_padding_top">@dimen/space_small_1</dimen>
<dimen name="layout_padding_bottom">@dimen/space_small_1</dimen>
<dimen name="layout_padding_end">@dimen/space_small_1</dimen>
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 8e1a51301..9b4d50b36 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -266,7 +266,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA
}
@Override
- public void showInspector(DocumentInfo doc) {
+ public void showPreview(DocumentInfo doc) {
throw new UnsupportedOperationException("Can't open properties.");
}
diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java
index c66a7e78c..190f5d960 100644
--- a/src/com/android/documentsui/ActionHandler.java
+++ b/src/com/android/documentsui/ActionHandler.java
@@ -118,7 +118,7 @@ public interface ActionHandler {
void showCreateDirectoryDialog();
- void showInspector(DocumentInfo doc);
+ void showPreview(DocumentInfo doc);
@Nullable DocumentInfo renameDocument(String name, DocumentInfo document);
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 4c25b3608..41fc5182f 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -525,11 +525,16 @@ public abstract class BaseActivity
root.setPadding(insets.getSystemWindowInsetLeft(),
insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
- View saveContainer = findViewById(R.id.container_save);
- saveContainer.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
-
- View rootsContainer = findViewById(R.id.container_roots);
- rootsContainer.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
+ // in M3, no additional bottom gap in full screen mode.
+ if (!useMaterial3()) {
+ View saveContainer = findViewById(R.id.container_save);
+ saveContainer.setPadding(
+ 0, 0, 0, insets.getSystemWindowInsetBottom());
+
+ View rootsContainer = findViewById(R.id.container_roots);
+ rootsContainer.setPadding(
+ 0, 0, 0, insets.getSystemWindowInsetBottom());
+ }
return insets.consumeSystemWindowInsets();
});
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index b4b08ce1b..ffa912c51 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -990,7 +990,7 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
? mActivity.getCurrentDirectory()
: mModel.getDocuments(selection).get(0);
- mActions.showInspector(doc);
+ mActions.showPreview(doc);
return true;
} else if (id == R.id.dir_menu_cut_to_clipboard) {
mActions.cutToClipboard();
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index 56eef8737..af5ef5bbc 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -19,7 +19,7 @@ package com.android.documentsui.files;
import static android.content.ContentResolver.wrap;
import static com.android.documentsui.base.SharedMinimal.DEBUG;
-import static com.android.documentsui.flags.Flags.desktopFileHandling;
+import com.android.documentsui.flags.Flags;
import android.app.DownloadManager;
import android.content.ActivityNotFoundException;
@@ -551,7 +551,7 @@ public class ActionHandler<T extends FragmentActivity & AbstractActionHandler.Co
return;
}
- if (desktopFileHandling()) {
+ if (Flags.desktopFileHandling()) {
Intent intent = buildViewIntent(doc);
intent.setComponent(
new ComponentName("android", "com.android.internal.app.ResolverActivity"));
@@ -571,8 +571,7 @@ public class ActionHandler<T extends FragmentActivity & AbstractActionHandler.Co
}
}
- @Override
- public void showInspector(DocumentInfo doc) {
+ private void showInspector(DocumentInfo doc) {
Metrics.logUserAction(MetricConsts.USER_ACTION_INSPECTOR);
Intent intent = InspectorActivity.createIntent(mActivity, doc.derivedUri, doc.userId);
@@ -596,4 +595,17 @@ public class ActionHandler<T extends FragmentActivity & AbstractActionHandler.Co
}
mActivity.startActivity(intent);
}
+
+ private void showPeek() {
+ Log.d(TAG, "Peek not implemented");
+ }
+
+ @Override
+ public void showPreview(DocumentInfo doc) {
+ if (Flags.useMaterial3() && Flags.usePeekPreview()) {
+ showPeek();
+ } else {
+ showInspector(doc);
+ }
+ }
}
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index cd744476e..4bb7c1ac5 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -350,7 +350,7 @@ public class FilesActivity extends BaseActivity implements AbstractActionHandler
} else if (id == R.id.option_menu_select_all) {
mInjector.actions.selectAllFiles();
} else if (id == R.id.option_menu_inspect) {
- mInjector.actions.showInspector(getCurrentDirectory());
+ mInjector.actions.showPreview(getCurrentDirectory());
} else {
final boolean ok = super.onOptionsItemSelected(item);
if (DEBUG && !ok) {
diff --git a/src/com/android/documentsui/picker/TrampolineActivity.kt b/src/com/android/documentsui/picker/TrampolineActivity.kt
new file mode 100644
index 000000000..6cb7d37a1
--- /dev/null
+++ b/src/com/android/documentsui/picker/TrampolineActivity.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.documentsui.picker
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.Intent.ACTION_GET_CONTENT
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.os.ext.SdkExtensions
+import android.provider.MediaStore.ACTION_PICK_IMAGES
+import androidx.appcompat.app.AppCompatActivity
+
+/**
+ * DocumentsUI PickActivity currently defers picking of media mime types to the Photopicker. This
+ * activity trampolines the intent to either Photopicker or to the PickActivity depending on whether
+ * there are non-media mime types to handle.
+ */
+class TrampolineActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceBundle: Bundle?) {
+ super.onCreate(savedInstanceBundle)
+
+ // This activity should not be present in the back stack nor should handle any of the
+ // corresponding results when picking items.
+ intent?.apply {
+ addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
+ addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP)
+ }
+
+ // In the event there is no photopicker returned, just refer to DocumentsUI.
+ val photopickerComponentName = getPhotopickerComponentName(intent.type)
+ if (photopickerComponentName == null) {
+ forwardIntentToDocumentsUI()
+ return
+ }
+
+ // The Photopicker has an entry point to take them back to DocumentsUI. In the event the
+ // user originated from Photopicker, we don't want to send them back.
+ val referredFromPhotopicker = referrer?.host == photopickerComponentName.packageName
+ if (referredFromPhotopicker || !shouldForwardIntentToPhotopicker(intent)) {
+ forwardIntentToDocumentsUI()
+ return
+ }
+
+ // Forward intent to Photopicker.
+ intent.setComponent(photopickerComponentName)
+ startActivity(intent)
+ finish()
+ }
+
+ private fun forwardIntentToDocumentsUI() {
+ intent.setClass(applicationContext, PickActivity::class.java)
+ startActivity(intent)
+ finish()
+ }
+
+ private fun getPhotopickerComponentName(type: String?): ComponentName? {
+ // Intent.ACTION_PICK_IMAGES is only available from SdkExtensions v2 onwards. Prior to that
+ // the Photopicker was not available, so in those cases should always send to DocumentsUI.
+ if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) < 2) {
+ return null
+ }
+
+ // Attempt to resolve the `ACTION_PICK_IMAGES` intent to get the Photopicker package.
+ // On T+ devices this is is a standalone package, whilst prior to T it is part of the
+ // MediaProvider module.
+ val pickImagesIntent = Intent(
+ ACTION_PICK_IMAGES
+ ).apply { addCategory(Intent.CATEGORY_DEFAULT) }
+ val photopickerComponentName: ComponentName? = pickImagesIntent.resolveActivity(
+ packageManager
+ )
+
+ // For certain devices the activity that handles ACTION_GET_CONTENT can be disabled (when
+ // the ACTION_PICK_IMAGES is enabled) so double check by explicitly checking the
+ // ACTION_GET_CONTENT activity on the same activity that handles ACTION_PICK_IMAGES.
+ val photopickerGetContentIntent = Intent(ACTION_GET_CONTENT).apply {
+ setType(type)
+ setPackage(photopickerComponentName?.packageName)
+ }
+ val photopickerGetContentComponent: ComponentName? =
+ photopickerGetContentIntent.resolveActivity(packageManager)
+
+ // Ensure the `ACTION_GET_CONTENT` activity is enabled.
+ if (!isComponentEnabled(photopickerGetContentComponent)) {
+ return null
+ }
+
+ return photopickerGetContentComponent
+ }
+
+ private fun isComponentEnabled(componentName: ComponentName?): Boolean {
+ if (componentName == null) {
+ return false
+ }
+
+ return when (packageManager.getComponentEnabledSetting(componentName)) {
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true
+ PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> {
+ // DEFAULT is a state that essentially defers to the state defined in the
+ // AndroidManifest which can be either enabled or disabled.
+ packageManager.getPackageInfo(
+ componentName.packageName,
+ PackageManager.GET_ACTIVITIES
+ )?.let { packageInfo: PackageInfo ->
+ if (packageInfo.activities == null) {
+ return false
+ }
+ for (val info in packageInfo.activities) {
+ if (info.name == componentName.className) {
+ return info.enabled
+ }
+ }
+ }
+ return false
+ }
+
+ // Everything else is considered disabled.
+ else -> false
+ }
+ }
+}
+
+fun shouldForwardIntentToPhotopicker(intent: Intent): Boolean {
+ if (intent.action != ACTION_GET_CONTENT || !isMediaMimeType(intent.type)) {
+ return false
+ }
+
+ // Intent has type ACTION_GET_CONTENT and is either image/* or video/* with no
+ // additional mime types.
+ if (!intent.hasExtra(Intent.EXTRA_MIME_TYPES)) {
+ return true
+ }
+
+ val extraMimeTypes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES)
+ extraMimeTypes?.let {
+ if (it.size == 0) {
+ return false
+ }
+
+ for (mimeType in it) {
+ if (!isMediaMimeType(mimeType)) {
+ return false
+ }
+ }
+ } ?: return false
+
+ return true
+}
+
+fun isMediaMimeType(mimeType: String?): Boolean {
+ return mimeType?.let { mimeType ->
+ mimeType.startsWith("image/") || mimeType.startsWith("video/")
+ } == true
+}
diff --git a/src/com/android/documentsui/ui/DocumentDebugInfo.java b/src/com/android/documentsui/ui/DocumentDebugInfo.java
deleted file mode 100644
index b7712c60a..000000000
--- a/src/com/android/documentsui/ui/DocumentDebugInfo.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 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.
- */
-
-package com.android.documentsui.ui;
-
-import androidx.annotation.Nullable;
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.TextView;
-
-import com.android.documentsui.base.DocumentInfo;
-
-/**
- * Document debug info view.
- */
-public class DocumentDebugInfo extends TextView {
- public DocumentDebugInfo(Context context) {
- super(context);
-
- }
-
- public DocumentDebugInfo(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- }
-
- public void update(DocumentInfo doc) {
-
- String dbgInfo = new StringBuilder()
- .append("** PROPERTIES **\n\n")
- .append("docid: " + doc.documentId).append("\n")
- .append("name: " + doc.displayName).append("\n")
- .append("mimetype: " + doc.mimeType).append("\n")
- .append("container: " + doc.isContainer()).append("\n")
- .append("virtual: " + doc.isVirtual()).append("\n")
- .append("\n")
- .append("** OPERATIONS **\n\n")
- .append("create: " + doc.isCreateSupported()).append("\n")
- .append("delete: " + doc.isDeleteSupported()).append("\n")
- .append("rename: " + doc.isRenameSupported()).append("\n\n")
- .append("** URI **\n\n")
- .append(doc.derivedUri).append("\n")
- .toString();
-
- setText(dbgInfo);
- }
-}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index d5e9a6045..c64b7aedd 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.documentsui.tests">
- <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/>
+ <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30"/>
<uses-permission android:name="android.permission.INTERNET" />
diff --git a/tests/common/com/android/documentsui/testing/TestModel.java b/tests/common/com/android/documentsui/testing/TestModel.java
index 452238189..83ece2ebb 100644
--- a/tests/common/com/android/documentsui/testing/TestModel.java
+++ b/tests/common/com/android/documentsui/testing/TestModel.java
@@ -40,7 +40,8 @@ public class TestModel extends Model {
Document.COLUMN_FLAGS,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_SIZE,
- Document.COLUMN_MIME_TYPE
+ Document.COLUMN_MIME_TYPE,
+ Document.COLUMN_LAST_MODIFIED
};
public final UserId mUserId;
@@ -104,7 +105,7 @@ public class TestModel extends Model {
}
public DocumentInfo createDocumentForUser(String name, String mimeType, int flags,
- UserId userId) {
+ long lastModified, UserId userId) {
DocumentInfo doc = new DocumentInfo();
doc.userId = userId;
doc.authority = mAuthority;
@@ -114,6 +115,7 @@ public class TestModel extends Model {
doc.mimeType = mimeType;
doc.flags = flags;
doc.size = mRand.nextInt();
+ doc.lastModified = lastModified;
addToCursor(doc);
@@ -121,7 +123,7 @@ public class TestModel extends Model {
}
public DocumentInfo createDocument(String name, String mimeType, int flags) {
- return createDocumentForUser(name, mimeType, flags, mUserId);
+ return createDocumentForUser(name, mimeType, flags, System.currentTimeMillis(), mUserId);
}
private void addToCursor(DocumentInfo doc) {
@@ -133,9 +135,17 @@ public class TestModel extends Model {
row.add(Document.COLUMN_MIME_TYPE, doc.mimeType);
row.add(Document.COLUMN_FLAGS, doc.flags);
row.add(Document.COLUMN_SIZE, doc.size);
+ row.add(Document.COLUMN_LAST_MODIFIED, doc.lastModified);
}
- private static String guessMimeType(String name) {
+ /**
+ * Attempts to guess the MIME type of the file based on its name. If unable to guess, returns
+ * "text/plain".
+ *
+ * @param name The name of the file whose MIME type is guessed.
+ * @return A guessed MIME type of "text/plain".
+ */
+ public static String guessMimeType(String name) {
int i = name.indexOf('.');
while(i != -1) {
diff --git a/tests/functional/com/android/documentsui/SortDocumentUiTest.java b/tests/functional/com/android/documentsui/SortDocumentUiTest.java
index 399867b89..b277884e3 100644
--- a/tests/functional/com/android/documentsui/SortDocumentUiTest.java
+++ b/tests/functional/com/android/documentsui/SortDocumentUiTest.java
@@ -20,10 +20,12 @@ import static com.android.documentsui.flags.Flags.FLAG_USE_MATERIAL3;
import android.net.Uri;
import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.view.KeyEvent;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.documentsui.files.FilesActivity;
import com.android.documentsui.sorting.SortDimension;
@@ -31,6 +33,7 @@ import com.android.documentsui.sorting.SortModel;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -73,6 +76,9 @@ public class SortDocumentUiTest extends ActivityTestJunit4<FilesActivity> {
return ret;
}
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
@Before
public void setUp() throws Exception {
super.setUp();
diff --git a/tests/functional/com/android/documentsui/TrampolineActivityTest.kt b/tests/functional/com/android/documentsui/TrampolineActivityTest.kt
new file mode 100644
index 000000000..c2201789b
--- /dev/null
+++ b/tests/functional/com/android/documentsui/TrampolineActivityTest.kt
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.documentsui
+
+import android.content.Intent
+import android.os.Build.VERSION_CODES
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import com.android.documentsui.flags.Flags.FLAG_REDIRECT_GET_CONTENT
+import com.android.documentsui.picker.TrampolineActivity
+import java.util.regex.Pattern
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Suite
+import org.junit.runners.Suite.SuiteClasses
+
+@SmallTest
+@RunWith(Suite::class)
+@SuiteClasses(
+ TrampolineActivityTest.ShouldLaunchCorrectPackageTest::class,
+ TrampolineActivityTest.RedirectTest::class
+)
+class TrampolineActivityTest() {
+ companion object {
+ const val UI_TIMEOUT = 5000L
+ val PHOTOPICKER_PACKAGE_REGEX: Pattern = Pattern.compile(".*photopicker.*")
+ val DOCUMENTSUI_PACKAGE_REGEX: Pattern = Pattern.compile(".*documentsui.*")
+
+ private var device: UiDevice? = null
+
+ @BeforeClass
+ @JvmStatic
+ fun setUp() {
+ device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ }
+ }
+
+ @RunWith(Parameterized::class)
+ @RequiresFlagsEnabled(FLAG_REDIRECT_GET_CONTENT)
+ class ShouldLaunchCorrectPackageTest {
+ enum class AppType {
+ PHOTOPICKER,
+ DOCUMENTSUI,
+ }
+
+ data class GetContentIntentData(
+ val mimeType: String,
+ val expectedApp: AppType,
+ val extraMimeTypes: Array<String>? = null,
+ ) {
+ override fun toString(): String {
+ if (extraMimeTypes != null) {
+ return "${mimeType}_${extraMimeTypes.joinToString("_")}"
+ }
+ return mimeType
+ }
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun parameters() =
+ listOf(
+ GetContentIntentData(
+ mimeType = "*/*",
+ expectedApp = AppType.DOCUMENTSUI,
+ ),
+ GetContentIntentData(
+ mimeType = "image/*",
+ expectedApp = AppType.PHOTOPICKER,
+ ),
+ GetContentIntentData(
+ mimeType = "video/*",
+ expectedApp = AppType.PHOTOPICKER,
+ ),
+ GetContentIntentData(
+ mimeType = "image/*",
+ extraMimeTypes = arrayOf("video/*"),
+ expectedApp = AppType.PHOTOPICKER,
+ ),
+ GetContentIntentData(
+ mimeType = "video/*",
+ extraMimeTypes = arrayOf("image/*"),
+ expectedApp = AppType.PHOTOPICKER,
+ ),
+ GetContentIntentData(
+ mimeType = "video/*",
+ extraMimeTypes = arrayOf("text/*"),
+ expectedApp = AppType.DOCUMENTSUI,
+ ),
+ GetContentIntentData(
+ mimeType = "video/*",
+ extraMimeTypes = arrayOf("image/*", "text/*"),
+ expectedApp = AppType.DOCUMENTSUI,
+ ),
+ GetContentIntentData(
+ mimeType = "*/*",
+ extraMimeTypes = arrayOf("image/*", "video/*"),
+ expectedApp = AppType.DOCUMENTSUI,
+ ),
+ GetContentIntentData(
+ mimeType = "image/*",
+ extraMimeTypes = arrayOf(),
+ expectedApp = AppType.DOCUMENTSUI,
+ )
+ )
+ }
+
+ @Parameterized.Parameter(0)
+ lateinit var testData: GetContentIntentData
+
+ @get:Rule
+ val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+ @Before
+ fun setUp() {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ intent.setClass(context, TrampolineActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ intent.setType(testData.mimeType)
+ testData.extraMimeTypes?.let { intent.putExtra(Intent.EXTRA_MIME_TYPES, it) }
+
+ context.startActivity(intent)
+ }
+
+ @Test
+ fun testCorrectAppIsLaunched() {
+ val bySelector = when (testData.expectedApp) {
+ AppType.PHOTOPICKER -> By.pkg(PHOTOPICKER_PACKAGE_REGEX)
+ else -> By.pkg(DOCUMENTSUI_PACKAGE_REGEX)
+ }
+
+ assertNotNull(device?.wait(Until.findObject(bySelector), UI_TIMEOUT))
+ }
+ }
+
+ @RunWith(AndroidJUnit4::class)
+ @RequiresFlagsEnabled(FLAG_REDIRECT_GET_CONTENT)
+ class RedirectTest {
+ @get:Rule
+ val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+ @Test
+ fun testReferredGetContentFromPhotopickerShouldNotRedirectBack() {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ intent.setClass(context, TrampolineActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ intent.setType("image/*")
+
+ context.startActivity(intent)
+ val moreButton = device?.wait(Until.findObject(By.desc("More")), UI_TIMEOUT)
+ moreButton?.click()
+
+ val browseButton = device?.wait(Until.findObject(By.textContains("Browse")), UI_TIMEOUT)
+ browseButton?.click()
+
+ assertNotNull(
+ "DocumentsUI has not launched",
+ device?.wait(Until.findObject(By.pkg(DOCUMENTSUI_PACKAGE_REGEX)), UI_TIMEOUT)
+ )
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = VERSION_CODES.S, maxSdkVersion = VERSION_CODES.S_V2)
+ fun testAndroidSWithTakeoverGetContentDisabledShouldNotReferToDocumentsUI() {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ intent.setClass(context, TrampolineActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ intent.setType("image/*")
+
+ try {
+ // Disable Photopicker from taking over `ACTION_GET_CONTENT`. In this situation, it
+ // should ALWAYS defer to DocumentsUI regardless if the mimetype satisfies the
+ // conditions.
+ device?.executeShellCommand(
+ "device_config put mediaprovider take_over_get_content false"
+ )
+ context.startActivity(intent)
+ assertNotNull(
+ device?.wait(Until.findObject(By.pkg(DOCUMENTSUI_PACKAGE_REGEX)), UI_TIMEOUT)
+ )
+ } finally {
+ device?.executeShellCommand(
+ "device_config delete mediaprovider take_over_get_content"
+ )
+ }
+ }
+ }
+}
diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
index 4e6cb8f05..2554ea52b 100644
--- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
@@ -724,8 +724,17 @@ public class ActionHandlerTest {
}
@Test
+ @RequiresFlagsEnabled({Flags.FLAG_USE_MATERIAL3, Flags.FLAG_USE_PEEK_PREVIEW})
+ public void testShowPeek() throws Exception {
+ mHandler.showPreview(TestEnv.FILE_GIF);
+ // The inspector activity is not called.
+ mActivity.startActivity.assertNotCalled();
+ }
+
+ @Test
+ @RequiresFlagsDisabled({Flags.FLAG_USE_PEEK_PREVIEW})
public void testShowInspector() throws Exception {
- mHandler.showInspector(TestEnv.FILE_GIF);
+ mHandler.showPreview(TestEnv.FILE_GIF);
mActivity.startActivity.assertCalled();
Intent intent = mActivity.startActivity.getLastValue();
@@ -737,10 +746,11 @@ public class ActionHandlerTest {
}
@Test
+ @RequiresFlagsDisabled({Flags.FLAG_USE_PEEK_PREVIEW})
public void testShowInspector_DebugDisabled() throws Exception {
mFeatures.debugSupport = false;
- mHandler.showInspector(TestEnv.FILE_GIF);
+ mHandler.showPreview(TestEnv.FILE_GIF);
Intent intent = mActivity.startActivity.getLastValue();
assertHasExtra(intent, Shared.EXTRA_SHOW_DEBUG);
@@ -748,11 +758,12 @@ public class ActionHandlerTest {
}
@Test
+ @RequiresFlagsDisabled({Flags.FLAG_USE_PEEK_PREVIEW})
public void testShowInspector_DebugEnabled() throws Exception {
mFeatures.debugSupport = true;
DebugFlags.setDocumentDetailsEnabled(true);
- mHandler.showInspector(TestEnv.FILE_GIF);
+ mHandler.showPreview(TestEnv.FILE_GIF);
Intent intent = mActivity.startActivity.getLastValue();
assertHasExtra(intent, Shared.EXTRA_SHOW_DEBUG);
@@ -761,6 +772,7 @@ public class ActionHandlerTest {
}
@Test
+ @RequiresFlagsDisabled({Flags.FLAG_USE_PEEK_PREVIEW})
public void testShowInspector_OverridesRootDocumentName() throws Exception {
mActivity.currentRoot = TestProvidersAccess.PICKLES;
mEnv.populateStack();
@@ -772,7 +784,7 @@ public class ActionHandlerTest {
DocumentInfo rootDoc = mEnv.state.stack.peek();
rootDoc.displayName = "poodles";
- mHandler.showInspector(rootDoc);
+ mHandler.showPreview(rootDoc);
Intent intent = mActivity.startActivity.getLastValue();
assertEquals(
TestProvidersAccess.PICKLES.title,
@@ -780,6 +792,7 @@ public class ActionHandlerTest {
}
@Test
+ @RequiresFlagsDisabled({Flags.FLAG_USE_PEEK_PREVIEW})
public void testShowInspector_OverridesRootDocumentNameX() throws Exception {
mActivity.currentRoot = TestProvidersAccess.PICKLES;
mEnv.populateStack();
@@ -792,7 +805,7 @@ public class ActionHandlerTest {
DocumentInfo rootDoc = mEnv.state.stack.peek();
rootDoc.displayName = "poodles";
- mHandler.showInspector(rootDoc);
+ mHandler.showPreview(rootDoc);
Intent intent = mActivity.startActivity.getLastValue();
assertFalse(intent.getExtras().containsKey(Intent.EXTRA_TITLE));
}
diff --git a/tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java b/tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java
index 14c86e69f..5adaa4d65 100644
--- a/tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java
+++ b/tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java
@@ -142,7 +142,7 @@ public class QuickViewIntentBuilderTest {
public void testBuild_twoProfiles_containsOnlyPreviewDocument() {
mEnv.model.reset();
mEnv.model.createDocumentForUser("a.txt", "text/plain", 0,
- TestProvidersAccess.OtherUser.USER_ID);
+ System.currentTimeMillis(), TestProvidersAccess.OtherUser.USER_ID);
DocumentInfo previewDoc = mEnv.model.createFile("b.png", 0);
mEnv.model.createFile("c.png", 0);
mEnv.model.update();
diff --git a/tests/unit/com/android/documentsui/loaders/BaseLoaderTest.kt b/tests/unit/com/android/documentsui/loaders/BaseLoaderTest.kt
index 71512e9a1..62434b71f 100644
--- a/tests/unit/com/android/documentsui/loaders/BaseLoaderTest.kt
+++ b/tests/unit/com/android/documentsui/loaders/BaseLoaderTest.kt
@@ -16,15 +16,20 @@
package com.android.documentsui.loaders
import android.os.Parcel
+import android.provider.DocumentsContract
import com.android.documentsui.DirectoryResult
import com.android.documentsui.TestActivity
import com.android.documentsui.TestConfigStore
import com.android.documentsui.base.DocumentInfo
+import com.android.documentsui.base.UserId
import com.android.documentsui.sorting.SortModel
import com.android.documentsui.testing.ActivityManagers
import com.android.documentsui.testing.TestEnv
+import com.android.documentsui.testing.TestModel
import com.android.documentsui.testing.UserManagers
+import java.time.Duration
import java.util.Locale
+import kotlin.time.Duration.Companion.hours
import org.junit.Before
/**
@@ -33,6 +38,20 @@ import org.junit.Before
fun getFileCount(result: DirectoryResult?) = result?.cursor?.count ?: -1
/**
+ * A data class that holds parameters that can be varied for the loader test. The last
+ * value, expectedCount, can be used for simple tests that check that the number of
+ * returned files matches the expectations.
+ */
+data class LoaderTestParams(
+ // A query, matched against file names. May be empty.
+ val query: String,
+ // The delta from now that indicates maximum age of matched files.
+ val lastModifiedDelta: Duration?,
+ // The number of files that are expected, for the above parameters, to be found by a loader.
+ val expectedCount: Int
+)
+
+/**
* Common base class for search and folder loaders.
*/
open class BaseLoaderTest {
@@ -54,11 +73,26 @@ open class BaseLoaderTest {
mActivity.userManager = UserManagers.create()
}
+ /**
+ * Creates a text, PNG, MP4 and MPG files named sample-000x, for x in 0 .. count - 1.
+ * Each file gets a matching extension. The 0th file is modified 1h, 1st 2 hours, .. etc., ago.
+ */
fun createDocuments(count: Int): Array<DocumentInfo> {
val extensionList = arrayOf("txt", "png", "mp4", "mpg")
+ val now = System.currentTimeMillis()
+ val flags = (DocumentsContract.Document.FLAG_SUPPORTS_WRITE
+ or DocumentsContract.Document.FLAG_SUPPORTS_DELETE
+ or DocumentsContract.Document.FLAG_SUPPORTS_RENAME)
return Array<DocumentInfo>(count) { i ->
val id = String.format(Locale.US, "%05d", i)
- mEnv.model.createFile("sample-$id.${extensionList[i % extensionList.size]}")
+ val name = "sample-$id.${extensionList[i % extensionList.size]}"
+ mEnv.model.createDocumentForUser(
+ name,
+ TestModel.guessMimeType(name),
+ flags,
+ now - 1.hours.inWholeMilliseconds * i,
+ UserId.DEFAULT_USER
+ )
}
}
}
diff --git a/tests/unit/com/android/documentsui/loaders/FolderLoaderTest.kt b/tests/unit/com/android/documentsui/loaders/FolderLoaderTest.kt
index 92aaaa041..cb0735b17 100644
--- a/tests/unit/com/android/documentsui/loaders/FolderLoaderTest.kt
+++ b/tests/unit/com/android/documentsui/loaders/FolderLoaderTest.kt
@@ -20,18 +20,44 @@ import com.android.documentsui.ContentLock
import com.android.documentsui.base.DocumentInfo
import com.android.documentsui.testing.TestFileTypeLookup
import com.android.documentsui.testing.TestProvidersAccess
+import java.time.Duration
import junit.framework.Assert.assertEquals
import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
+private const val TOTAL_FILE_COUNT = 10
+
+@RunWith(Parameterized::class)
@SmallTest
-class FolderLoaderTest : BaseLoaderTest() {
+class FolderLoaderTest(private val testParams: LoaderTestParams) : BaseLoaderTest() {
+ companion object {
+ @JvmStatic
+ @Parameters(name = "with parameters {0}")
+ fun data() = listOf(
+ LoaderTestParams("", null, TOTAL_FILE_COUNT),
+ // The first file is at NOW, the second at NOW - 1h, etc.
+ LoaderTestParams("", Duration.ofMinutes(1L), 1),
+ LoaderTestParams("", Duration.ofMinutes(60L + 1), 2),
+ LoaderTestParams("", Duration.ofMinutes(TOTAL_FILE_COUNT * 60L + 1), TOTAL_FILE_COUNT),
+ )
+ }
+
@Test
fun testLoadInBackground() {
val mockProvider = mEnv.mockProviders[TestProvidersAccess.DOWNLOADS.authority]
- val docs = createDocuments(5)
+ val docs = createDocuments(TOTAL_FILE_COUNT)
mockProvider!!.setNextChildDocumentsReturns(*docs)
val userIds = listOf(TestProvidersAccess.DOWNLOADS.userId)
- val queryOptions = QueryOptions(10, null, null, true, arrayOf<String>("*/*"))
+ val queryOptions =
+ QueryOptions(
+ TOTAL_FILE_COUNT,
+ testParams.lastModifiedDelta,
+ null,
+ true,
+ arrayOf<String>("*/*")
+ )
val contentLock = ContentLock()
// TODO(majewski): Is there a better way to create Downloads root folder DocumentInfo?
val rootFolderInfo = DocumentInfo()
@@ -50,6 +76,6 @@ class FolderLoaderTest : BaseLoaderTest() {
mEnv.state.sortModel
)
val directoryResult = loader.loadInBackground()
- assertEquals(docs.size, getFileCount(directoryResult))
+ assertEquals(testParams.expectedCount, getFileCount(directoryResult))
}
}
diff --git a/tests/unit/com/android/documentsui/loaders/SearchLoaderTest.kt b/tests/unit/com/android/documentsui/loaders/SearchLoaderTest.kt
index 6d78ffdd9..9addf1a7f 100644
--- a/tests/unit/com/android/documentsui/loaders/SearchLoaderTest.kt
+++ b/tests/unit/com/android/documentsui/loaders/SearchLoaderTest.kt
@@ -15,19 +15,41 @@
*/
package com.android.documentsui.loaders
+import androidx.test.filters.SmallTest
import com.android.documentsui.ContentLock
import com.android.documentsui.LockingContentObserver
import com.android.documentsui.base.DocumentInfo
import com.android.documentsui.testing.TestFileTypeLookup
import com.android.documentsui.testing.TestProvidersAccess
+import java.time.Duration
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import junit.framework.Assert.assertEquals
import org.junit.Before
import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
-class SearchLoaderTest : BaseLoaderTest() {
- lateinit var mExecutor: ExecutorService
+private const val TOTAL_FILE_COUNT = 8
+
+@RunWith(Parameterized::class)
+@SmallTest
+class SearchLoaderTest(private val testParams: LoaderTestParams) : BaseLoaderTest() {
+ private lateinit var mExecutor: ExecutorService
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "with parameters {0}")
+ fun data() = listOf(
+ LoaderTestParams("sample", null, TOTAL_FILE_COUNT),
+ LoaderTestParams("txt", null, 2),
+ LoaderTestParams("foozig", null, 0),
+ // The first file is at NOW, the second at NOW - 1h; expect 2.
+ LoaderTestParams("sample", Duration.ofMinutes(60 + 1), 2),
+ // TODO(b:378590632): Add test for recents.
+ )
+ }
@Before
override fun setUp() {
@@ -38,10 +60,17 @@ class SearchLoaderTest : BaseLoaderTest() {
@Test
fun testLoadInBackground() {
val mockProvider = mEnv.mockProviders[TestProvidersAccess.DOWNLOADS.authority]
- val docs = createDocuments(8)
+ val docs = createDocuments(TOTAL_FILE_COUNT)
mockProvider!!.setNextChildDocumentsReturns(*docs)
val userIds = listOf(TestProvidersAccess.DOWNLOADS.userId)
- val queryOptions = QueryOptions(10, null, null, true, arrayOf("*/*"))
+ val queryOptions =
+ QueryOptions(
+ TOTAL_FILE_COUNT + 1,
+ testParams.lastModifiedDelta,
+ null,
+ true,
+ arrayOf("*/*")
+ )
val contentLock = ContentLock()
val rootIds = listOf(TestProvidersAccess.DOWNLOADS)
val observer = LockingContentObserver(contentLock) {
@@ -59,13 +88,12 @@ class SearchLoaderTest : BaseLoaderTest() {
TestFileTypeLookup(),
observer,
rootIds,
- "txt",
+ testParams.query,
queryOptions,
mEnv.state.sortModel,
mExecutor,
)
val directoryResult = loader.loadInBackground()
- // Expect only 2 text files to match txt.
- assertEquals(2, getFileCount(directoryResult))
+ assertEquals(testParams.expectedCount, getFileCount(directoryResult))
}
}
diff --git a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
index 12753cd89..d3e6e90d0 100644
--- a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
@@ -645,7 +645,8 @@ public class ActionHandlerTest {
mActivity.currentRoot = TestProvidersAccess.OtherUser.DOWNLOADS;
mEnv.model.reset();
DocumentInfo otherUserDoc = mEnv.model.createDocumentForUser("a.png",
- "image/png", /* flags= */ 0, TestProvidersAccess.OtherUser.USER_ID);
+ "image/png", /* flags= */ 0, System.currentTimeMillis(),
+ TestProvidersAccess.OtherUser.USER_ID);
mEnv.model.update();
mHandler.onDocumentOpened(otherUserDoc, ActionHandler.VIEW_TYPE_PREVIEW,