summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml10
-rw-r--r--flags.aconfig3
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/grid_nameplate_background.xml25
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/grid_thumbnail_background.xml24
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/item_doc_grid.xml286
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values/dimens.xml16
-rw-r--r--res/values/strings.xml3
-rw-r--r--src/com/android/documentsui/NavigationViewManager.java3
-rw-r--r--src/com/android/documentsui/dirlist/DirectoryFragment.java6
-rw-r--r--src/com/android/documentsui/dirlist/DocumentHolder.java2
-rw-r--r--src/com/android/documentsui/dirlist/GridDocumentHolder.java93
-rw-r--r--src/com/android/documentsui/dirlist/ListDocumentHolder.java2
-rw-r--r--src/com/android/documentsui/loaders/SearchLoader.kt2
-rw-r--r--tests/common/com/android/documentsui/bots/UiBot.java6
-rw-r--r--tests/functional/com/android/documentsui/FileManagementUiTest.java15
-rw-r--r--tests/functional/com/android/documentsui/TrampolineActivityTest.kt6
-rw-r--r--tests/unit/com/android/documentsui/files/MenuManagerTest.java14
-rw-r--r--tests/unit/com/android/documentsui/ui/MessageBuilderTest.kt234
18 files changed, 559 insertions, 191 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 1fa1ac3b6..944b27471 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -63,7 +63,7 @@
android:name=".picker.TrampolineActivity"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay"
- android:featureFlag="com.android.documentsui.flags.redirect_get_content"
+ android:featureFlag="com.android.documentsui.flags.redirect_get_content_ro"
android:visibleToInstantApps="true">
<intent-filter android:priority="120">
<action android:name="android.intent.action.OPEN_DOCUMENT" />
@@ -95,7 +95,7 @@
android:theme="@style/LauncherTheme"
android:visibleToInstantApps="true">
<intent-filter
- android:featureFlag="!com.android.documentsui.flags.redirect_get_content"
+ android:featureFlag="!com.android.documentsui.flags.redirect_get_content_ro"
android:priority="100">
<action android:name="android.intent.action.OPEN_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
@@ -103,7 +103,7 @@
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter
- android:featureFlag="!com.android.documentsui.flags.redirect_get_content"
+ android:featureFlag="!com.android.documentsui.flags.redirect_get_content_ro"
android:priority="100">
<action android:name="android.intent.action.CREATE_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
@@ -111,7 +111,7 @@
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter
- android:featureFlag="!com.android.documentsui.flags.redirect_get_content"
+ android:featureFlag="!com.android.documentsui.flags.redirect_get_content_ro"
android:priority="100">
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
@@ -119,7 +119,7 @@
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter
- android:featureFlag="!com.android.documentsui.flags.redirect_get_content"
+ android:featureFlag="!com.android.documentsui.flags.redirect_get_content_ro"
android:priority="100">
<action android:name="android.intent.action.OPEN_DOCUMENT_TREE" />
<category android:name="android.intent.category.DEFAULT" />
diff --git a/flags.aconfig b/flags.aconfig
index 560d791b4..165253b1e 100644
--- a/flags.aconfig
+++ b/flags.aconfig
@@ -45,10 +45,11 @@ flag {
}
flag {
- name: "redirect_get_content"
+ name: "redirect_get_content_ro"
namespace: "documentsui"
description: "Redirects GET_CONTENT requests to Photopicker when appropriate"
bug: "377771195"
+ is_fixed_read_only: true
}
flag {
diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/grid_nameplate_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/grid_nameplate_background.xml
new file mode 100644
index 000000000..4a25b9a02
--- /dev/null
+++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/grid_nameplate_background.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- selected -->
+ <item android:state_selected="true">
+ <shape>
+ <corners android:radius="@dimen/grid_item_nameplate_radius" />
+ <solid android:color="?attr/colorPrimaryContainer" />
+ </shape>
+ </item>
+</selector> \ No newline at end of file
diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/grid_thumbnail_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/grid_thumbnail_background.xml
new file mode 100644
index 000000000..aad18471b
--- /dev/null
+++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/grid_thumbnail_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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- selected -->
+ <item android:state_selected="true">
+ <shape>
+ <corners android:radius="@dimen/grid_item_thumbnail_radius" />
+ <solid android:color="?attr/colorPrimaryContainer" />
+ </shape>
+ </item>
+</selector> \ No newline at end of file
diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/item_doc_grid.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/item_doc_grid.xml
index 32596f2f4..dd88f50b5 100644
--- a/res/flag(com.android.documentsui.flags.use_material3)/layout/item_doc_grid.xml
+++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/item_doc_grid.xml
@@ -14,196 +14,170 @@
limitations under the License.
-->
-<!-- FYI: This layout has an extra top level container view that was previously used
- to allow for the insertion of debug info. The debug info is now gone, but the
- container remains because there is a high likelihood of UI regression relating
- to focus and selection states, some of which are specific to keyboard
- when touch mode is not enable. So, if you, heroic engineer of the future,
- decide to rip these out, please be sure to check out focus and keyboards. -->
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/item_root"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_margin="4dp"
- android:foreground="?android:attr/selectableItemBackground"
+ android:layout_width="@dimen/grid_item_width"
+ android:layout_height="@dimen/grid_item_height"
+ android:layout_margin="@dimen/grid_item_layout_margin"
android:clickable="true"
android:focusable="true"
- app:cardElevation="0dp">
+ app:cardBackgroundColor="@android:color/transparent"
+ app:cardElevation="0dp"
+ app:strokeWidth="0dp">
- <com.google.android.material.card.MaterialCardView
+ <RelativeLayout
+ android:id="@+id/grid_item_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:elevation="0dp"
- android:duplicateParentState="true"
- app:cardElevation="0dp"
- app:strokeWidth="1dp"
- app:strokeColor="?android:strokeColor">
-
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:duplicateParentState="true">
-
- <!-- Main item thumbnail. Comprised of two overlapping images, the
- visibility of which is controlled by code in
- DirectoryFragment.java. -->
-
- <FrameLayout
- android:id="@+id/thumbnail"
- android:background="?attr/gridItemTint"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
+ android:layout_marginStart="@dimen/grid_item_layout_marginStart"
+ android:layout_marginEnd="@dimen/grid_item_layout_marginEnd"
+ android:layout_marginTop="@dimen/grid_item_layout_marginTop">
+
+ <!-- Main item thumbnail. Comprised of two overlapping images, the
+ visibility of which is controlled by code in
+ DirectoryFragment.java. -->
+
+ <FrameLayout
+ android:id="@+id/thumbnail"
+ android:layout_width="@dimen/grid_item_thumbnail_width"
+ android:layout_height="@dimen/grid_item_thumbnail_height"
+ android:layout_centerHorizontal="true"
+ android:background="@drawable/grid_thumbnail_background">
+
+ <!-- stroke width will be controlled dynamically in the code. -->
+ <com.google.android.material.card.MaterialCardView
+ android:id="@+id/icon_wrapper"
+ android:layout_width="@dimen/grid_item_icon_width"
+ android:layout_height="@dimen/grid_item_icon_height"
+ android:layout_gravity="center"
+ app:cardBackgroundColor="?attr/colorSurfaceContainerLowest"
+ app:cardElevation="0dp"
+ app:strokeColor="?attr/colorSecondaryContainer"
+ app:strokeWidth="0dp">
<com.android.documentsui.GridItemThumbnail
android:id="@+id/icon_thumb"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:scaleType="centerCrop"
+ android:layout_height="match_parent"
android:contentDescription="@null"
+ android:scaleType="centerCrop"
android:tint="?attr/gridItemTint"
- android:tintMode="src_over"/>
+ android:tintMode="src_over" />
<com.android.documentsui.GridItemThumbnail
android:id="@+id/icon_mime_lg"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:layout_gravity="center"
- android:scaleType="fitCenter"
- android:contentDescription="@null"/>
-
- </FrameLayout>
-
- <FrameLayout
- android:id="@+id/preview_icon"
- android:layout_width="@dimen/button_touch_size"
- android:layout_height="@dimen/button_touch_size"
- android:layout_alignParentTop="true"
- android:layout_alignParentEnd="true"
- android:pointerIcon="hand"
- android:focusable="true"
- android:clickable="true">
+ android:contentDescription="@null"
+ android:scaleType="fitCenter" />
+
+ </com.google.android.material.card.MaterialCardView>
+
+ </FrameLayout>
+
+ <FrameLayout
+ android:id="@+id/preview_icon"
+ android:layout_width="@dimen/button_touch_size"
+ android:layout_height="@dimen/button_touch_size"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentTop="true"
+ android:clickable="true"
+ android:focusable="true"
+ android:pointerIcon="hand">
+
+ <ImageView
+ android:layout_width="@dimen/zoom_icon_size"
+ android:layout_height="@dimen/zoom_icon_size"
+ android:layout_gravity="center"
+ android:background="@drawable/circle_button_background"
+ android:padding="2dp"
+ android:scaleType="fitCenter"
+ android:src="@drawable/ic_zoom_out" />
+
+ </FrameLayout>
+
+ <!-- Item nameplate. Has some text fields (title, size, mod-time, etc). -->
+
+ <LinearLayout
+ android:id="@+id/nameplate"
+ android:layout_width="@dimen/grid_item_nameplate_width"
+ android:layout_height="@dimen/grid_item_nameplate_height"
+ android:layout_below="@id/thumbnail"
+ android:layout_marginTop="@dimen/grid_item_nameplate_marginTop"
+ android:background="@drawable/grid_nameplate_background"
+ android:orientation="vertical"
+ android:padding="@dimen/grid_item_nameplate_padding">
+
+ <!-- Top row. -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
<ImageView
- android:layout_width="@dimen/zoom_icon_size"
- android:layout_height="@dimen/zoom_icon_size"
- android:padding="2dp"
- android:layout_gravity="center"
- android:background="@drawable/circle_button_background"
- android:scaleType="fitCenter"
- android:src="@drawable/ic_zoom_out"/>
-
- </FrameLayout>
+ android:id="@+id/icon_profile_badge"
+ android:layout_width="@dimen/briefcase_icon_size"
+ android:layout_height="@dimen/briefcase_icon_size"
+ android:layout_marginEnd="@dimen/briefcase_icon_margin"
+ android:contentDescription="@string/a11y_work"
+ android:gravity="center_vertical"
+ android:src="@drawable/ic_briefcase"
+ android:tint="?android:attr/colorAccent" />
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAlignment="center"
+ android:textAppearance="@style/FileItemLabelText" />
- <!-- Item nameplate. Has a mime-type icon and some text fields (title,
- size, mod-time, etc). -->
+ </LinearLayout>
+ <!-- Bottom row. -->
<LinearLayout
- android:id="@+id/nameplate"
- android:background="?android:attr/colorBackground"
- android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_below="@id/thumbnail">
+ android:gravity="center"
+ android:orientation="horizontal">
- <FrameLayout
- android:id="@+id/icon"
+ <TextView
+ android:id="@+id/details"
android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_centerVertical="true"
- android:pointerIcon="hand"
- android:paddingTop="8dp"
- android:paddingBottom="8dp"
- android:paddingStart="12dp"
- android:paddingEnd="8dp">
-
- <ImageView
- android:id="@+id/icon_mime_sm"
- android:layout_width="@dimen/grid_item_icon_size"
- android:layout_height="@dimen/grid_item_icon_size"
- android:layout_gravity="center"
- android:scaleType="center"
- android:contentDescription="@null"/>
-
- <ImageView
- android:id="@+id/icon_check"
- android:src="@drawable/ic_check_circle"
- android:alpha="0"
- android:layout_width="@dimen/check_icon_size"
- android:layout_height="@dimen/check_icon_size"
- android:layout_gravity="center"
- android:scaleType="fitCenter"
- android:contentDescription="@null"/>
-
- </FrameLayout>
-
- <RelativeLayout
- android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingBottom="8dp"
- android:paddingTop="8dp"
- android:paddingEnd="12dp">
-
- <ImageView
- android:id="@+id/icon_profile_badge"
- android:layout_height="@dimen/briefcase_icon_size"
- android:layout_width="@dimen/briefcase_icon_size"
- android:layout_marginEnd="@dimen/briefcase_icon_margin"
- android:layout_alignTop="@android:id/title"
- android:layout_alignBottom="@android:id/title"
- android:gravity="center_vertical"
- android:src="@drawable/ic_briefcase"
- android:tint="?android:attr/colorAccent"
- android:contentDescription="@string/a11y_work"/>
-
- <TextView
- android:id="@android:id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
- android:layout_toEndOf="@+id/icon_profile_badge"
- android:singleLine="true"
- android:ellipsize="end"
- android:textAlignment="viewStart"
- android:textAppearance="@style/CardPrimaryText"/>
-
- <TextView
- android:id="@+id/details"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@android:id/title"
- android:layout_marginEnd="4dp"
- android:singleLine="true"
- android:ellipsize="end"
- android:textAlignment="viewStart"
- android:textAppearance="@style/ItemCaptionText" />
-
- <TextView
- android:id="@+id/date"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@android:id/title"
- android:layout_toEndOf="@id/details"
- android:singleLine="true"
- android:ellipsize="end"
- android:textAlignment="viewStart"
- android:textAppearance="@style/ItemCaptionText" />
-
- </RelativeLayout>
+ android:layout_marginEnd="4dp"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/ItemCaptionText" />
- </LinearLayout>
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="4dp"
+ android:singleLine="true"
+ android:text="@string/bullet"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/ItemCaptionText" />
+
+ <TextView
+ android:id="@+id/date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/ItemCaptionText" />
- </RelativeLayout>
+ </LinearLayout>
- </com.google.android.material.card.MaterialCardView>
+ </LinearLayout>
- <!-- An overlay that draws the item border when it is focused. -->
- <View
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@drawable/item_doc_grid_border_rounded"
- android:contentDescription="@null"
- android:duplicateParentState="true"/>
+ </RelativeLayout>
</com.google.android.material.card.MaterialCardView>
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 1ba7b00cd..b11397ee9 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
@@ -66,6 +66,22 @@
<dimen name="breadcrumb_item_height">36dp</dimen>
<dimen name="dir_elevation">8dp</dimen>
<dimen name="drag_shadow_size">120dp</dimen>
+ <dimen name="grid_item_width">150dp</dimen>
+ <dimen name="grid_item_height">132dp</dimen>
+ <dimen name="grid_item_layout_marginStart">@dimen/space_extra_small_2</dimen>
+ <dimen name="grid_item_layout_marginEnd">@dimen/space_extra_small_2</dimen>
+ <dimen name="grid_item_layout_marginTop">@dimen/space_extra_small_2</dimen>
+ <dimen name="grid_item_thumbnail_width">80dp</dimen>
+ <dimen name="grid_item_thumbnail_height">80dp</dimen>
+ <dimen name="grid_item_thumbnail_radius">12dp</dimen>
+ <dimen name="grid_item_icon_width">64dp</dimen>
+ <dimen name="grid_item_icon_height">64dp</dimen>
+ <dimen name="grid_item_layout_margin">@dimen/space_small_1</dimen>
+ <dimen name="grid_item_nameplate_width">142dp</dimen>
+ <dimen name="grid_item_nameplate_height">44dp</dimen>
+ <dimen name="grid_item_nameplate_padding">4dp</dimen>
+ <dimen name="grid_item_nameplate_marginTop">@dimen/space_extra_small_2</dimen>
+ <dimen name="grid_item_nameplate_radius">8dp</dimen>
<dimen name="grid_item_elevation">2dp</dimen>
<dimen name="grid_item_radius">12dp</dimen>
<dimen name="max_drawer_width">280dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index cd7ad917c..8a147f2d4 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -604,4 +604,7 @@
<!-- Accessibility announcement when switching to list mode of files and directories shown. [CHAR_LIMIT=100] -->
<string name="list_mode_showing">Showing in list mode.</string>
+ <!-- Unicode Character “•” (U+2022). -->
+ <string name="bullet">\u2022</string>
+
</resources>
diff --git a/src/com/android/documentsui/NavigationViewManager.java b/src/com/android/documentsui/NavigationViewManager.java
index bd139ec7a..6d56b4590 100644
--- a/src/com/android/documentsui/NavigationViewManager.java
+++ b/src/com/android/documentsui/NavigationViewManager.java
@@ -322,6 +322,9 @@ public class NavigationViewManager extends SelectionTracker.SelectionObserver<St
if (showBurgerMenuOnToolbar) {
mToolbar.setNavigationIcon(getActionBarIcon());
mToolbar.setNavigationContentDescription(R.string.drawer_open);
+ } else {
+ mToolbar.setNavigationIcon(null);
+ mToolbar.setNavigationContentDescription(null);
}
if (shouldShowSearchBar()) {
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 078498153..9cd7c2f7c 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -1535,7 +1535,11 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
// For orientation changed case, sometimes the docs loading comes after the menu
// update. We need to update the menu here to ensure the status is correct.
mInjector.menuManager.updateModel(mModel);
- mInjector.menuManager.updateOptionMenu();
+ if (useMaterial3()) {
+ mActivity.getNavigator().updateActionMenu();
+ } else {
+ mInjector.menuManager.updateOptionMenu();
+ }
if (VersionUtils.isAtLeastS()) {
mActivity.updateHeader(update.hasCrossProfileException());
} else {
diff --git a/src/com/android/documentsui/dirlist/DocumentHolder.java b/src/com/android/documentsui/dirlist/DocumentHolder.java
index 69058fb5a..b1b2765f8 100644
--- a/src/com/android/documentsui/dirlist/DocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/DocumentHolder.java
@@ -58,6 +58,8 @@ public abstract class DocumentHolder
static final float DISABLED_ALPHA = useMaterial3() ? 0.6f : 0.3f;
+ static final int THUMBNAIL_STROKE_WIDTH = useMaterial3() ? 2 : 0;
+
protected final Context mContext;
protected @Nullable String mModelId;
diff --git a/src/com/android/documentsui/dirlist/GridDocumentHolder.java b/src/com/android/documentsui/dirlist/GridDocumentHolder.java
index a55d56e5f..112e98702 100644
--- a/src/com/android/documentsui/dirlist/GridDocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/GridDocumentHolder.java
@@ -21,6 +21,7 @@ import static com.android.documentsui.DevicePolicyResources.Drawables.WORK_PROFI
import static com.android.documentsui.base.DocumentInfo.getCursorInt;
import static com.android.documentsui.base.DocumentInfo.getCursorLong;
import static com.android.documentsui.base.DocumentInfo.getCursorString;
+import static com.android.documentsui.flags.Flags.useMaterial3;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
@@ -35,6 +36,7 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.android.documentsui.ConfigStore;
@@ -47,6 +49,8 @@ import com.android.documentsui.roots.RootCursorWrapper;
import com.android.documentsui.ui.Views;
import com.android.modules.utils.build.SdkLevel;
+import com.google.android.material.card.MaterialCardView;
+
import java.util.Map;
import java.util.function.Function;
@@ -56,29 +60,44 @@ final class GridDocumentHolder extends DocumentHolder {
final TextView mDate;
final TextView mDetails;
final ImageView mIconMimeLg;
- final ImageView mIconMimeSm;
+ // Null when useMaterial3 flag is ON.
+ final @Nullable ImageView mIconMimeSm;
final ImageView mIconThumb;
- final ImageView mIconCheck;
+ // Null when useMaterial3 flag is ON.
+ final @Nullable ImageView mIconCheck;
final ImageView mIconBadge;
final IconHelper mIconHelper;
- final View mIconLayout;
+ // Null when useMaterial3 flag is ON.
+ final @Nullable View mIconLayout;
final View mPreviewIcon;
// This is used in as a convenience in our bind method.
private final DocumentInfo mDoc = new DocumentInfo();
+ // Non-null only when useMaterial3 flag is ON.
+ private final @Nullable MaterialCardView mIconWrapper;
+
GridDocumentHolder(Context context, ViewGroup parent, IconHelper iconHelper,
ConfigStore configStore) {
super(context, parent, R.layout.item_doc_grid, configStore);
- mIconLayout = itemView.findViewById(R.id.icon);
+ if (useMaterial3()) {
+ mIconWrapper = itemView.findViewById(R.id.icon_wrapper);
+ mIconLayout = null;
+ mIconMimeSm = null;
+ mIconCheck = null;
+ } else {
+ mIconWrapper = null;
+ mIconLayout = itemView.findViewById(R.id.icon);
+ mIconMimeSm = (ImageView) itemView.findViewById(R.id.icon_mime_sm);
+ mIconCheck = (ImageView) itemView.findViewById(R.id.icon_check);
+ }
+
mTitle = (TextView) itemView.findViewById(android.R.id.title);
mDate = (TextView) itemView.findViewById(R.id.date);
mDetails = (TextView) itemView.findViewById(R.id.details);
mIconMimeLg = (ImageView) itemView.findViewById(R.id.icon_mime_lg);
- mIconMimeSm = (ImageView) itemView.findViewById(R.id.icon_mime_sm);
mIconThumb = (ImageView) itemView.findViewById(R.id.icon_thumb);
- mIconCheck = (ImageView) itemView.findViewById(R.id.icon_check);
mIconBadge = (ImageView) itemView.findViewById(R.id.icon_profile_badge);
mPreviewIcon = itemView.findViewById(R.id.preview_icon);
@@ -99,17 +118,20 @@ final class GridDocumentHolder extends DocumentHolder {
@Override
public void setSelected(boolean selected, boolean animate) {
- // We always want to make sure our check box disappears if we're not selected,
- // even if the item is disabled. This is because this object can be reused
- // and this method will be called to setup initial state.
float checkAlpha = selected ? 1f : 0f;
- if (animate) {
- fade(mIconMimeSm, checkAlpha).start();
- fade(mIconCheck, checkAlpha).start();
- } else {
- mIconCheck.setAlpha(checkAlpha);
+ if (!useMaterial3()) {
+ // We always want to make sure our check box disappears if we're not selected,
+ // even if the item is disabled. This is because this object can be reused
+ // and this method will be called to setup initial state.
+ if (animate) {
+ fade(mIconMimeSm, checkAlpha).start();
+ fade(mIconCheck, checkAlpha).start();
+ } else {
+ mIconCheck.setAlpha(checkAlpha);
+ }
}
+
// But it should be an error to be set to selected && be disabled.
if (!itemView.isEnabled()) {
assert (!selected);
@@ -117,10 +139,21 @@ final class GridDocumentHolder extends DocumentHolder {
super.setSelected(selected, animate);
- if (animate) {
- fade(mIconMimeSm, 1f - checkAlpha).start();
- } else {
- mIconMimeSm.setAlpha(1f - checkAlpha);
+ if (!useMaterial3()) {
+ if (animate) {
+ fade(mIconMimeSm, 1f - checkAlpha).start();
+ } else {
+ mIconMimeSm.setAlpha(1f - checkAlpha);
+ }
+ }
+
+ // Do not show stroke when selected, only show stroke when not selected if it has thumbnail.
+ if (mIconWrapper != null) {
+ if (selected) {
+ mIconWrapper.setStrokeWidth(0);
+ } else if (mIconThumb.getDrawable() != null) {
+ mIconWrapper.setStrokeWidth(THUMBNAIL_STROKE_WIDTH);
+ }
}
}
@@ -131,7 +164,9 @@ final class GridDocumentHolder extends DocumentHolder {
float imgAlpha = enabled ? 1f : DISABLED_ALPHA;
mIconMimeLg.setAlpha(imgAlpha);
- mIconMimeSm.setAlpha(imgAlpha);
+ if (!useMaterial3()) {
+ mIconMimeSm.setAlpha(imgAlpha);
+ }
mIconThumb.setAlpha(imgAlpha);
}
@@ -171,6 +206,9 @@ final class GridDocumentHolder extends DocumentHolder {
@Override
public boolean inSelectRegion(MotionEvent event) {
+ if (useMaterial3()) {
+ return Views.isEventOver(event, itemView.getParent(), mIconWrapper);
+ }
return Views.isEventOver(event, itemView.getParent(), mIconLayout);
}
@@ -202,8 +240,21 @@ final class GridDocumentHolder extends DocumentHolder {
mIconThumb.animate().cancel();
mIconThumb.setAlpha(0f);
- mIconHelper.load(
- mDoc, mIconThumb, mIconMimeLg, mIconMimeSm, /* thumbnailLoadedCallback= */ null);
+ if (useMaterial3()) {
+ mIconHelper.load(
+ mDoc, mIconThumb, mIconMimeLg, /* subIconMime= */ null,
+ thumbnailLoaded -> {
+ // Show stroke when thumbnail is loaded.
+ if (mIconWrapper != null) {
+ mIconWrapper.setStrokeWidth(
+ thumbnailLoaded ? THUMBNAIL_STROKE_WIDTH : 0);
+ }
+ });
+ } else {
+ mIconHelper.load(
+ mDoc, mIconThumb, mIconMimeLg, mIconMimeSm, /* thumbnailLoadedCallback= */
+ null);
+ }
mTitle.setText(mDoc.displayName, TextView.BufferType.SPANNABLE);
mTitle.setVisibility(View.VISIBLE);
diff --git a/src/com/android/documentsui/dirlist/ListDocumentHolder.java b/src/com/android/documentsui/dirlist/ListDocumentHolder.java
index 2fbbabc3e..a4839d4f4 100644
--- a/src/com/android/documentsui/dirlist/ListDocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/ListDocumentHolder.java
@@ -270,7 +270,7 @@ final class ListDocumentHolder extends DocumentHolder {
thumbnailLoaded -> {
// Show stroke when thumbnail is loaded.
if (useMaterial3() && mIconWrapper != null) {
- mIconWrapper.setStrokeWidth(thumbnailLoaded ? 2 : 0);
+ mIconWrapper.setStrokeWidth(thumbnailLoaded ? THUMBNAIL_STROKE_WIDTH : 0);
}
});
diff --git a/src/com/android/documentsui/loaders/SearchLoader.kt b/src/com/android/documentsui/loaders/SearchLoader.kt
index b17271bdc..f0da924e2 100644
--- a/src/com/android/documentsui/loaders/SearchLoader.kt
+++ b/src/com/android/documentsui/loaders/SearchLoader.kt
@@ -101,7 +101,7 @@ class SearchLoader(
}
@Volatile
- private lateinit var mSearchTaskList: List<SearchTask>
+ private var mSearchTaskList: List<SearchTask> = listOf()
// Creates a directory result object corresponding to the current parameters of the loader.
override fun loadInBackground(): DirectoryResult? {
diff --git a/tests/common/com/android/documentsui/bots/UiBot.java b/tests/common/com/android/documentsui/bots/UiBot.java
index 4ec75bdc6..161510e2a 100644
--- a/tests/common/com/android/documentsui/bots/UiBot.java
+++ b/tests/common/com/android/documentsui/bots/UiBot.java
@@ -284,14 +284,16 @@ public class UiBot extends Bots.BaseBot {
// Espresso has flaky results when keyboard shows up, so hiding it for now
// before trying to click on any dialog button
Espresso.closeSoftKeyboard();
- onView(withId(android.R.id.button1)).perform(click());
+ UiObject2 okButton = mDevice.findObject(By.res("android:id/button1"));
+ okButton.click();
}
public void clickDialogCancelButton() throws UiObjectNotFoundException {
// Espresso has flaky results when keyboard shows up, so hiding it for now
// before trying to click on any dialog button
Espresso.closeSoftKeyboard();
- onView(withId(android.R.id.button2)).perform(click());
+ UiObject2 okButton = mDevice.findObject(By.res("android:id/button2"));
+ okButton.click();
}
public UiObject findMenuLabelWithName(String label) {
diff --git a/tests/functional/com/android/documentsui/FileManagementUiTest.java b/tests/functional/com/android/documentsui/FileManagementUiTest.java
index 4bbd8c6eb..43b74bd33 100644
--- a/tests/functional/com/android/documentsui/FileManagementUiTest.java
+++ b/tests/functional/com/android/documentsui/FileManagementUiTest.java
@@ -124,6 +124,21 @@ public class FileManagementUiTest extends ActivityTest<FilesActivity> {
bots.directory.waitForDocument("file1.png");
}
+ @HugeLongTest
+ public void testKeyboard_PasteDocumentWhileSelectionActive() throws Exception {
+ bots.directory.selectDocument("file1.png", 1);
+ bots.keyboard.pressKey(KeyEvent.KEYCODE_C, KeyEvent.META_CTRL_ON);
+
+ device.waitForIdle();
+ bots.directory.openDocument("Dir1");
+ bots.directory.selectDocument("ChildDir1", 1);
+
+ bots.keyboard.pressKey(KeyEvent.KEYCODE_V, KeyEvent.META_CTRL_ON);
+ device.waitForIdle();
+
+ bots.directory.assertDocumentsPresent("file1.png");
+ }
+
public void testDeleteDocument_Cancel() throws Exception {
bots.directory.selectDocument("file1.png", 1);
device.waitForIdle();
diff --git a/tests/functional/com/android/documentsui/TrampolineActivityTest.kt b/tests/functional/com/android/documentsui/TrampolineActivityTest.kt
index 76d703701..6bf0975ad 100644
--- a/tests/functional/com/android/documentsui/TrampolineActivityTest.kt
+++ b/tests/functional/com/android/documentsui/TrampolineActivityTest.kt
@@ -30,7 +30,7 @@ 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.flags.Flags.FLAG_REDIRECT_GET_CONTENT_RO
import com.android.documentsui.picker.TrampolineActivity
import java.util.Optional
import java.util.regex.Pattern
@@ -79,7 +79,7 @@ class TrampolineActivityTest() {
}
@RunWith(Parameterized::class)
- @RequiresFlagsEnabled(FLAG_REDIRECT_GET_CONTENT)
+ @RequiresFlagsEnabled(FLAG_REDIRECT_GET_CONTENT_RO)
class ShouldLaunchCorrectPackageTest {
enum class AppType {
PHOTOPICKER,
@@ -203,7 +203,7 @@ class TrampolineActivityTest() {
}
@RunWith(AndroidJUnit4::class)
- @RequiresFlagsEnabled(FLAG_REDIRECT_GET_CONTENT)
+ @RequiresFlagsEnabled(FLAG_REDIRECT_GET_CONTENT_RO)
class RedirectTest {
@get:Rule
val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
diff --git a/tests/unit/com/android/documentsui/files/MenuManagerTest.java b/tests/unit/com/android/documentsui/files/MenuManagerTest.java
index f3b7078e8..394537784 100644
--- a/tests/unit/com/android/documentsui/files/MenuManagerTest.java
+++ b/tests/unit/com/android/documentsui/files/MenuManagerTest.java
@@ -489,6 +489,20 @@ public final class MenuManagerTest {
}
@Test
+ public void testOptionMenu_ExtractAll() {
+ dirDetails.isInArchive = true;
+ mgr.updateOptionMenu(testMenu);
+ if (Flags.zipNg()) {
+ mOptionExtractAll.assertEnabledAndVisible();
+ } else {
+ mOptionExtractAll.assertDisabledAndInvisible();
+ }
+ dirDetails.isInArchive = false;
+ mgr.updateOptionMenu(testMenu);
+ mOptionExtractAll.assertDisabledAndInvisible();
+ }
+
+ @Test
public void testInflateContextMenu_Files() {
TestMenuInflater inflater = new TestMenuInflater();
diff --git a/tests/unit/com/android/documentsui/ui/MessageBuilderTest.kt b/tests/unit/com/android/documentsui/ui/MessageBuilderTest.kt
new file mode 100644
index 000000000..63ff80dad
--- /dev/null
+++ b/tests/unit/com/android/documentsui/ui/MessageBuilderTest.kt
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2025 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 android.content.Context
+import android.content.res.Resources
+import android.net.Uri
+import android.provider.DocumentsContract.Document.MIME_TYPE_DIR
+import androidx.test.filters.SmallTest
+import com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_CONVERTED
+import com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_FAILURE
+import com.android.documentsui.R
+import com.android.documentsui.base.DocumentInfo
+import com.android.documentsui.services.FileOperationService.OPERATION_COMPRESS
+import com.android.documentsui.services.FileOperationService.OPERATION_COPY
+import com.android.documentsui.services.FileOperationService.OPERATION_DELETE
+import com.android.documentsui.services.FileOperationService.OPERATION_EXTRACT
+import com.android.documentsui.services.FileOperationService.OPERATION_MOVE
+import com.android.documentsui.services.FileOperationService.OPERATION_UNKNOWN
+import org.junit.Assert.assertEquals
+import org.junit.Before
+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
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mock
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(Suite::class)
+@SuiteClasses(
+ MessageBuilderTest.GenerateDeleteMessage::class,
+ MessageBuilderTest.GenerateListMessage::class
+)
+open class MessageBuilderTest() {
+ companion object {
+ const val EXPECTED_MESSAGE = "Delete message"
+ }
+
+ class GenerateDeleteMessage() {
+ private lateinit var messageBuilder: MessageBuilder
+
+ @Mock
+ private lateinit var resources: Resources
+
+ @Mock
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.openMocks(this)
+ whenever(context.resources).thenReturn(resources)
+ messageBuilder = MessageBuilder(context)
+ }
+
+ private fun assertDeleteMessage(docInfo: DocumentInfo, resId: Int) {
+ whenever(
+ resources.getString(
+ eq(resId),
+ eq(docInfo.displayName)
+ )
+ ).thenReturn(EXPECTED_MESSAGE)
+ assertEquals(messageBuilder.generateDeleteMessage(listOf(docInfo)), EXPECTED_MESSAGE)
+ }
+
+ private fun assertQuantityDeleteMessage(docInfos: List<DocumentInfo>, resId: Int) {
+ whenever(
+ resources.getQuantityString(
+ eq(resId),
+ eq(docInfos.size),
+ eq(docInfos.size)
+ )
+ ).thenReturn(EXPECTED_MESSAGE)
+ assertEquals(messageBuilder.generateDeleteMessage(docInfos), EXPECTED_MESSAGE)
+ }
+
+ @Test
+ fun testGenerateDeleteMessage_singleFile() {
+ assertDeleteMessage(
+ createFile("Test doc"),
+ R.string.delete_filename_confirmation_message
+ )
+ }
+
+ @Test
+ fun testGenerateDeleteMessage_singleDirectory() {
+ assertDeleteMessage(
+ createDirectory("Test doc"),
+ R.string.delete_foldername_confirmation_message
+ )
+ }
+
+ @Test
+ fun testGenerateDeleteMessage_multipleFiles() {
+ assertQuantityDeleteMessage(
+ listOf(createFile("File 1"), createFile("File 2")),
+ R.plurals.delete_files_confirmation_message
+ )
+ }
+
+ @Test
+ fun testGenerateDeleteMessage_multipleDirectories() {
+ assertQuantityDeleteMessage(
+ listOf(
+ createDirectory("Directory 1"),
+ createDirectory("Directory 2")
+ ),
+ R.plurals.delete_folders_confirmation_message
+ )
+ }
+
+ @Test
+ fun testGenerateDeleteMessage_mixedFilesAndDirectories() {
+ assertQuantityDeleteMessage(
+ listOf(createFile("File 1"), createDirectory("Directory 1")),
+ R.plurals.delete_items_confirmation_message
+ )
+ }
+ }
+
+ @RunWith(Parameterized::class)
+ class GenerateListMessage() {
+ private lateinit var messageBuilder: MessageBuilder
+
+ @Mock
+ private lateinit var resources: Resources
+
+ @Mock
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.openMocks(this)
+ whenever(context.resources).thenReturn(resources)
+ messageBuilder = MessageBuilder(context)
+ }
+
+ data class ListMessageData(
+ val dialogType: Int,
+ val opType: Int = OPERATION_UNKNOWN,
+ val resId: Int
+ )
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun parameters() =
+ listOf(
+ ListMessageData(
+ dialogType = DIALOG_TYPE_CONVERTED,
+ resId = R.plurals.copy_converted_warning_content,
+ ),
+ ListMessageData(
+ dialogType = DIALOG_TYPE_FAILURE,
+ opType = OPERATION_COPY,
+ resId = R.plurals.copy_failure_alert_content,
+ ),
+ ListMessageData(
+ dialogType = DIALOG_TYPE_FAILURE,
+ opType = OPERATION_COMPRESS,
+ resId = R.plurals.compress_failure_alert_content,
+ ),
+ ListMessageData(
+ dialogType = DIALOG_TYPE_FAILURE,
+ opType = OPERATION_EXTRACT,
+ resId = R.plurals.extract_failure_alert_content,
+ ),
+ ListMessageData(
+ dialogType = DIALOG_TYPE_FAILURE,
+ opType = OPERATION_DELETE,
+ resId = R.plurals.delete_failure_alert_content,
+ ),
+ ListMessageData(
+ dialogType = DIALOG_TYPE_FAILURE,
+ opType = OPERATION_MOVE,
+ resId = R.plurals.move_failure_alert_content,
+ ),
+ )
+ }
+
+ @Parameterized.Parameter(0)
+ lateinit var testData: ListMessageData
+
+ @Test
+ fun testGenerateListMessage() {
+ whenever(
+ resources.getQuantityString(
+ eq(testData.resId),
+ eq(2),
+ anyString(),
+ )
+ ).thenReturn(EXPECTED_MESSAGE)
+ assertEquals(
+ messageBuilder.generateListMessage(
+ testData.dialogType,
+ testData.opType,
+ listOf(createFile("File 1")),
+ listOf(Uri.parse("content://random-uri")),
+ ),
+ EXPECTED_MESSAGE
+ )
+ }
+ }
+}
+
+fun createFile(displayName: String): DocumentInfo {
+ val doc = DocumentInfo()
+ doc.displayName = displayName
+ return doc
+}
+
+fun createDirectory(displayName: String): DocumentInfo {
+ val doc = DocumentInfo()
+ doc.displayName = displayName
+ doc.mimeType = MIME_TYPE_DIR
+ return doc
+}