diff options
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, |