diff options
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 +} |