summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/com/android/documentsui/AbstractActionHandler.java13
-rw-r--r--src/com/android/documentsui/BaseActivity.java41
-rw-r--r--src/com/android/documentsui/DragAndDropManager.java10
-rw-r--r--src/com/android/documentsui/DragShadowBuilder.java142
-rw-r--r--src/com/android/documentsui/HorizontalBreadcrumb.java57
-rw-r--r--src/com/android/documentsui/IconUtils.java81
-rw-r--r--src/com/android/documentsui/JobPanelController.kt126
-rw-r--r--src/com/android/documentsui/MenuManager.java52
-rw-r--r--src/com/android/documentsui/NavigationViewManager.java28
-rw-r--r--src/com/android/documentsui/ProfileTabs.java7
-rw-r--r--src/com/android/documentsui/UserManagerState.java264
-rw-r--r--src/com/android/documentsui/archives/ArchiveRegistry.java11
-rw-r--r--src/com/android/documentsui/archives/ArchivesProvider.java13
-rw-r--r--src/com/android/documentsui/base/DocumentInfo.java5
-rw-r--r--src/com/android/documentsui/base/UserId.java7
-rw-r--r--src/com/android/documentsui/dirlist/DirectoryAddonsAdapter.java7
-rw-r--r--src/com/android/documentsui/dirlist/DirectoryFragment.java20
-rw-r--r--src/com/android/documentsui/dirlist/DocumentHolder.java2
-rw-r--r--src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java22
-rw-r--r--src/com/android/documentsui/dirlist/GridDocumentHolder.java22
-rw-r--r--src/com/android/documentsui/dirlist/ListDocumentHolder.java19
-rw-r--r--src/com/android/documentsui/dirlist/SelectionMetadata.java23
-rw-r--r--src/com/android/documentsui/files/FilesActivity.java10
-rw-r--r--src/com/android/documentsui/files/MenuManager.java29
-rw-r--r--src/com/android/documentsui/loaders/BaseFileLoader.kt12
-rw-r--r--src/com/android/documentsui/loaders/FolderLoader.kt2
-rw-r--r--src/com/android/documentsui/picker/PickActivity.java4
-rw-r--r--src/com/android/documentsui/picker/PickFragment.java29
-rw-r--r--src/com/android/documentsui/picker/SaveFragment.java41
-rw-r--r--src/com/android/documentsui/queries/SearchChipViewManager.java7
-rw-r--r--src/com/android/documentsui/services/FileOperationService.java10
-rw-r--r--src/com/android/documentsui/services/Job.java10
-rw-r--r--src/com/android/documentsui/sorting/TableHeaderController.java25
-rw-r--r--src/com/android/documentsui/util/ColorUtils.kt36
-rw-r--r--src/com/android/documentsui/util/FlagUtils.kt6
35 files changed, 921 insertions, 272 deletions
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index de193e235..89e9bc2d6 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -20,7 +20,8 @@ import static com.android.documentsui.base.DocumentInfo.getCursorInt;
import static com.android.documentsui.base.DocumentInfo.getCursorString;
import static com.android.documentsui.base.SharedMinimal.DEBUG;
import static com.android.documentsui.util.FlagUtils.isDesktopFileHandlingFlagEnabled;
-import static com.android.documentsui.util.FlagUtils.isUseSearchV2RwFlagEnabled;
+import static com.android.documentsui.util.FlagUtils.isUseSearchV2FlagEnabled;
+import static com.android.documentsui.util.FlagUtils.isZipNgFlagEnabled;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
@@ -458,17 +459,17 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA
private boolean viewDocument(DocumentInfo doc) {
if (doc.isPartial()) {
- Log.w(TAG, "Can't view partial file.");
+ Log.w(TAG, "Cannot view partial file");
return false;
}
- if (doc.isInArchive()) {
- Log.w(TAG, "Can't view files in archives.");
+ if (!isZipNgFlagEnabled() && doc.isInArchive()) {
+ Log.w(TAG, "Cannot view file in archive");
return false;
}
if (doc.isDirectory()) {
- Log.w(TAG, "Can't view directories.");
+ Log.w(TAG, "Cannot view directory");
return true;
}
@@ -916,7 +917,7 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA
mState.stack.changeRoot(mActivity.getCurrentRoot());
}
- if (isUseSearchV2RwFlagEnabled()) {
+ if (isUseSearchV2FlagEnabled()) {
return onCreateLoaderV2(id, args);
}
return onCreateLoaderV1(id, args);
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 0b5d96da9..790feeac4 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -569,23 +569,32 @@ public abstract class BaseActivity
View root = findViewById(R.id.coordinator_layout);
root.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
- root.setOnApplyWindowInsetsListener((v, insets) -> {
- root.setPadding(insets.getSystemWindowInsetLeft(),
- insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
-
- // When use_material3 flag is ON, no additional bottom gap in full screen mode.
- if (!isUseMaterial3FlagEnabled()) {
- 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());
- }
+ root.setOnApplyWindowInsetsListener(
+ (v, insets) -> {
+ root.setPadding(
+ insets.getSystemWindowInsetLeft(),
+ insets.getSystemWindowInsetTop(),
+ insets.getSystemWindowInsetRight(),
+ 0);
+
+ // When use_material3 flag is ON and FEATURE_FREEFORM_WINDOW_MANAGEMENT is
+ // enabled, then there should not be any additional bottom gap in full screen
+ // mode. Otherwise need to take into account the system window insets such as
+ // the bottom swipe up navigation gesture.
+ if (!isUseMaterial3FlagEnabled()
+ || !getApplicationContext()
+ .getPackageManager()
+ .hasSystemFeature(
+ PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT)) {
+ 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();
- });
+ return insets.consumeSystemWindowInsets();
+ });
getWindow().setNavigationBarDividerColor(Color.TRANSPARENT);
if (Build.VERSION.SDK_INT >= 29) {
diff --git a/src/com/android/documentsui/DragAndDropManager.java b/src/com/android/documentsui/DragAndDropManager.java
index bed8764ae..158d43c95 100644
--- a/src/com/android/documentsui/DragAndDropManager.java
+++ b/src/com/android/documentsui/DragAndDropManager.java
@@ -16,6 +16,8 @@
package com.android.documentsui;
+import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled;
+
import android.content.ClipData;
import android.content.Context;
import android.graphics.drawable.Drawable;
@@ -277,7 +279,9 @@ public interface DragAndDropManager {
final Drawable icon;
final int size = srcs.size();
- if (size == 1) {
+ // If use_material3 flag is ON, we always show the icon/title for the first file even
+ // when we have multiple files.
+ if (size == 1 || isUseMaterial3FlagEnabled()) {
DocumentInfo doc = srcs.get(0);
title = doc.displayName;
icon = iconHelper.getDocumentIcon(mContext, doc);
@@ -287,6 +291,10 @@ public interface DragAndDropManager {
icon = mDefaultShadowIcon;
}
+ if (isUseMaterial3FlagEnabled()) {
+ mShadowBuilder.updateDragFileCount(size);
+ }
+
mShadowBuilder.updateTitle(title);
mShadowBuilder.updateIcon(icon);
diff --git a/src/com/android/documentsui/DragShadowBuilder.java b/src/com/android/documentsui/DragShadowBuilder.java
index 10a0106d5..be76302e8 100644
--- a/src/com/android/documentsui/DragShadowBuilder.java
+++ b/src/com/android/documentsui/DragShadowBuilder.java
@@ -16,7 +16,7 @@
package com.android.documentsui;
-import com.android.documentsui.DragAndDropManager.State;
+import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled;
import android.content.Context;
import android.graphics.Canvas;
@@ -29,6 +29,12 @@ import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
+import androidx.annotation.Nullable;
+
+import com.android.documentsui.DragAndDropManager.State;
+
+import java.text.NumberFormat;
+
class DragShadowBuilder extends View.DragShadowBuilder {
private final View mShadowView;
@@ -37,8 +43,18 @@ class DragShadowBuilder extends View.DragShadowBuilder {
private final int mWidth;
private final int mHeight;
private final int mShadowRadius;
- private int mPadding;
+ private final int mPadding;
private Paint paint;
+ // This will be null if use_material3 flag is OFF.
+ private final @Nullable View mAdditionalShadowView;
+ // This will always be 0 if the use_material3 flag is OFF.
+ private int mDragFileCount = 0;
+ // The following 5 dimensions will be 0 if the use_material3 flag is OFF.
+ private final int mDragContentRadius;
+ private final int mAdditionalLayerOffset;
+ private final int mDragFileCounterOffset;
+ private final int mShadow2Radius;
+ private final int mShadowYOffset;
DragShadowBuilder(Context context) {
mWidth = context.getResources().getDimensionPixelSize(R.dimen.drag_shadow_width);
@@ -49,6 +65,28 @@ class DragShadowBuilder extends View.DragShadowBuilder {
mShadowView = LayoutInflater.from(context).inflate(R.layout.drag_shadow_layout, null);
mTitle = (TextView) mShadowView.findViewById(android.R.id.title);
mIcon = (DropBadgeView) mShadowView.findViewById(android.R.id.icon);
+ if (isUseMaterial3FlagEnabled()) {
+ mAdditionalShadowView =
+ LayoutInflater.from(context).inflate(R.layout.additional_drag_shadow, null);
+ mDragContentRadius =
+ context.getResources().getDimensionPixelSize(R.dimen.drag_content_radius);
+ mAdditionalLayerOffset =
+ context.getResources()
+ .getDimensionPixelSize(R.dimen.drag_additional_layer_offset);
+ mDragFileCounterOffset =
+ context.getResources().getDimensionPixelSize(R.dimen.drag_file_counter_offset);
+ mShadow2Radius =
+ context.getResources().getDimensionPixelSize(R.dimen.drag_shadow_2_radius);
+ mShadowYOffset =
+ context.getResources().getDimensionPixelSize(R.dimen.drag_shadow_y_offset);
+ } else {
+ mAdditionalShadowView = null;
+ mDragContentRadius = 0;
+ mAdditionalLayerOffset = 0;
+ mDragFileCounterOffset = 0;
+ mShadow2Radius = 0;
+ mShadowYOffset = 0;
+ }
// Important for certain APIs
mShadowView.setLayerType(View.LAYER_TYPE_SOFTWARE, paint);
@@ -67,23 +105,86 @@ class DragShadowBuilder extends View.DragShadowBuilder {
Rect r = canvas.getClipBounds();
// Calling measure is necessary in order for all child views to get correctly laid out.
mShadowView.measure(
- View.MeasureSpec.makeMeasureSpec(r.right- r.left, View.MeasureSpec.EXACTLY),
+ View.MeasureSpec.makeMeasureSpec(r.right - r.left, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(r.bottom - r.top , View.MeasureSpec.EXACTLY));
mShadowView.layout(r.left, r.top, r.right, r.bottom);
// Since DragShadow is not an actual view drawn in hardware-accelerated window,
// android:elevation does not work; we need to draw the shadow ourselves manually.
paint.setColor(Color.TRANSPARENT);
- // Shadow 1
- int opacity = (int) (255 * 0.1);
- paint.setShadowLayer(mShadowRadius, 0, 0, Color.argb(opacity, 0, 0, 0));
- canvas.drawRect(r.left + mPadding, r.top + mPadding, r.right - mPadding,
- r.bottom - mPadding, paint);
- // Shadow 2
- opacity = (int) (255 * 0.24);
- paint.setShadowLayer(mShadowRadius, 0, mShadowRadius, Color.argb(opacity, 0, 0, 0));
- canvas.drawRect(r.left + mPadding, r.top + mPadding, r.right - mPadding,
- r.bottom - mPadding, paint);
+
+ // Layers on the canvas (from bottom to top):
+ // 1. Two shadows for the additional drag layer (if drag file count > 1)
+ // 2. The additional layer view itself (if drag file count > 1)
+ // 3. Two shadows for the drag content layer (icon, title)
+ // 4. The drag content layer itself
+ final int shadowOneOpacity = (int) (255 * 0.15);
+ final int shadowTwoOpacity = (int) (255 * 0.30);
+ if (mAdditionalShadowView != null && mDragFileCount > 1) {
+ mAdditionalShadowView.measure(
+ View.MeasureSpec.makeMeasureSpec(r.right - r.left, View.MeasureSpec.EXACTLY),
+ View.MeasureSpec.makeMeasureSpec(r.bottom - r.top , View.MeasureSpec.EXACTLY));
+ mAdditionalShadowView.layout(r.left, r.top, r.right, r.bottom);
+ // Shadow 1
+ paint.setShadowLayer(
+ mShadowRadius, 0, mShadowYOffset, Color.argb(shadowOneOpacity, 0, 0, 0));
+ canvas.drawRoundRect(
+ r.left + mShadowRadius,
+ r.top + mDragFileCounterOffset + mAdditionalLayerOffset,
+ r.right - mDragFileCounterOffset - mAdditionalLayerOffset,
+ r.bottom - mShadowRadius,
+ mDragContentRadius,
+ mDragContentRadius,
+ paint);
+ // Shadow 2
+ paint.setShadowLayer(
+ mShadow2Radius, 0, mShadowYOffset, Color.argb(shadowTwoOpacity, 0, 0, 0));
+ canvas.drawRoundRect(
+ r.left + mShadowRadius,
+ r.top + mDragFileCounterOffset + mAdditionalLayerOffset,
+ r.right - mDragFileCounterOffset - mAdditionalLayerOffset,
+ r.bottom - mShadowRadius,
+ mDragContentRadius,
+ mDragContentRadius,
+ paint);
+ mAdditionalShadowView.draw(canvas);
+ }
+
+ if (isUseMaterial3FlagEnabled()) {
+ // Shadow 1
+ paint.setShadowLayer(
+ mShadowRadius, 0, mShadowYOffset, Color.argb(shadowOneOpacity, 0, 0, 0));
+ canvas.drawRoundRect(
+ r.left + mShadowRadius + mAdditionalLayerOffset,
+ r.top + mDragFileCounterOffset,
+ r.right - mDragFileCounterOffset,
+ r.bottom - mShadowRadius - mAdditionalLayerOffset,
+ mDragContentRadius,
+ mDragContentRadius,
+ paint);
+ // Shadow 2
+ paint.setShadowLayer(
+ mShadow2Radius, 0, mShadowYOffset, Color.argb(shadowTwoOpacity, 0, 0, 0));
+ canvas.drawRoundRect(
+ r.left + mShadowRadius + mAdditionalLayerOffset,
+ r.top + mDragFileCounterOffset,
+ r.right - mDragFileCounterOffset,
+ r.bottom - mShadowRadius - mAdditionalLayerOffset,
+ mDragContentRadius,
+ mDragContentRadius,
+ paint);
+ } else {
+ // Shadow 1
+ int opacity = (int) (255 * 0.1);
+ paint.setShadowLayer(mShadowRadius, 0, 0, Color.argb(opacity, 0, 0, 0));
+ canvas.drawRect(r.left + mPadding, r.top + mPadding, r.right - mPadding,
+ r.bottom - mPadding, paint);
+ // Shadow 2
+ opacity = (int) (255 * 0.24);
+ paint.setShadowLayer(mShadowRadius, 0, mShadowRadius, Color.argb(opacity, 0, 0, 0));
+ canvas.drawRect(r.left + mPadding, r.top + mPadding, r.right - mPadding,
+ r.bottom - mPadding, paint);
+ }
mShadowView.draw(canvas);
}
@@ -98,4 +199,19 @@ class DragShadowBuilder extends View.DragShadowBuilder {
void onStateUpdated(@State int state) {
mIcon.updateState(state);
}
+
+ void updateDragFileCount(int count) {
+ if (!isUseMaterial3FlagEnabled()) {
+ return;
+ }
+ mDragFileCount = count;
+ TextView dragFileCountView = mShadowView.findViewById(R.id.drag_file_counter);
+ if (dragFileCountView != null) {
+ dragFileCountView.setVisibility(count > 1 ? View.VISIBLE : View.GONE);
+ if (count > 1) {
+ NumberFormat numberFormat = NumberFormat.getInstance();
+ dragFileCountView.setText(numberFormat.format(count));
+ }
+ }
+ }
}
diff --git a/src/com/android/documentsui/HorizontalBreadcrumb.java b/src/com/android/documentsui/HorizontalBreadcrumb.java
index 94f0e13f9..9d2fc723e 100644
--- a/src/com/android/documentsui/HorizontalBreadcrumb.java
+++ b/src/com/android/documentsui/HorizontalBreadcrumb.java
@@ -16,6 +16,8 @@
package com.android.documentsui;
+import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled;
+
import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
@@ -25,6 +27,7 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
+import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -45,6 +48,9 @@ public final class HorizontalBreadcrumb extends RecyclerView implements Breadcru
private LinearLayoutManager mLayoutManager;
private BreadcrumbAdapter mAdapter;
private IntConsumer mClickListener;
+ // Represents the top divider (border) of the breadcrumb on the compact size screen.
+ // It will be null on other screen sizes, or when the use_material3 flag is OFF.
+ private @Nullable View mTopDividerView;
public HorizontalBreadcrumb(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
@@ -61,12 +67,14 @@ public final class HorizontalBreadcrumb extends RecyclerView implements Breadcru
@Override
public void setup(Environment env,
com.android.documentsui.base.State state,
- IntConsumer listener) {
+ IntConsumer listener,
+ @Nullable View topDivider) {
mClickListener = listener;
mLayoutManager = new HorizontalBreadcrumbLinearLayoutManager(
getContext(), LinearLayoutManager.HORIZONTAL, false);
mAdapter = new BreadcrumbAdapter(state, env, this::onKey);
+ mTopDividerView = topDivider;
// Since we are using GestureDetector to detect click events, a11y services don't know which
// views are clickable because we aren't using View.OnClickListener. Thus, we need to use a
// custom accessibility delegate to route click events correctly.
@@ -109,6 +117,9 @@ public final class HorizontalBreadcrumb extends RecyclerView implements Breadcru
setVisibility(GONE);
setAdapter(null);
}
+ if (mTopDividerView != null) {
+ mTopDividerView.setVisibility(visibility ? VISIBLE : GONE);
+ }
mAdapter.updateLastItemSize();
}
@@ -174,8 +185,6 @@ public final class HorizontalBreadcrumb extends RecyclerView implements Breadcru
@Override
public void onBindViewHolder(BreadcrumbHolder holder, int position) {
- final int padding = (int) holder.itemView.getResources()
- .getDimension(R.dimen.breadcrumb_item_padding);
final boolean isFirst = position == 0;
// Note that when isFirst is true, there might not be a DocumentInfo on the stack as it
// could be an error state screen accessible from the root info.
@@ -183,9 +192,45 @@ public final class HorizontalBreadcrumb extends RecyclerView implements Breadcru
holder.mTitle.setText(
isFirst ? mEnv.getCurrentRoot().title : mState.stack.get(position).displayName);
- holder.mTitle.setEnabled(isLast);
- holder.mTitle.setPadding(isFirst ? padding * 3 : padding,
- padding, isLast ? padding * 2 : padding, padding);
+ if (isUseMaterial3FlagEnabled()) {
+ // The last path part in the breadcrumb is not clickable.
+ holder.mTitle.setEnabled(!isLast);
+ } else {
+ holder.mTitle.setEnabled(isLast);
+ }
+ if (isUseMaterial3FlagEnabled()) {
+ final int paddingHorizontal =
+ (int)
+ holder.itemView
+ .getResources()
+ .getDimension(R.dimen.breadcrumb_item_padding_horizontal);
+ final int paddingVertical =
+ (int)
+ holder.itemView
+ .getResources()
+ .getDimension(R.dimen.breadcrumb_item_padding_vertical);
+ final int arrowPadding =
+ (int)
+ holder.itemView
+ .getResources()
+ .getDimension(R.dimen.breadcrumb_item_arrow_padding);
+ holder.mTitle.setPadding(
+ paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical);
+
+ ViewGroup.MarginLayoutParams params =
+ (ViewGroup.MarginLayoutParams) holder.mArrow.getLayoutParams();
+ params.setMarginStart(arrowPadding);
+ params.setMarginEnd(arrowPadding);
+ holder.mArrow.setLayoutParams(params);
+ } else {
+ final int padding = (int) holder.itemView.getResources()
+ .getDimension(R.dimen.breadcrumb_item_padding);
+ holder.mTitle.setPadding(
+ isFirst ? padding * 3 : padding,
+ padding,
+ isLast ? padding * 2 : padding,
+ padding);
+ }
holder.mArrow.setVisibility(isLast ? View.GONE : View.VISIBLE);
holder.itemView.setOnKeyListener(mClickListener);
diff --git a/src/com/android/documentsui/IconUtils.java b/src/com/android/documentsui/IconUtils.java
index 1763fd2a3..477209150 100644
--- a/src/com/android/documentsui/IconUtils.java
+++ b/src/com/android/documentsui/IconUtils.java
@@ -16,15 +16,56 @@
package com.android.documentsui;
+import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled;
+
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
+import android.content.res.Resources;
+import android.graphics.Outline;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.ImageView;
import com.android.documentsui.base.UserId;
+import com.android.documentsui.util.ColorUtils;
+
+import java.util.HashMap;
+import java.util.Map;
public class IconUtils {
+ // key: drawable resource id, value: color attribute id
+ private static final Map<Integer, Integer> sCustomIconColorMap = new HashMap<>();
+
+ static {
+ if (isUseMaterial3FlagEnabled()) {
+ // Use Resources.getSystem().getIdentifier() here instead of R.drawable.ic_doc_folder
+ // because com.android.internal.R is not public.
+ sCustomIconColorMap.put(
+ Resources.getSystem().getIdentifier("ic_doc_folder", "drawable", "android"),
+ com.google.android.material.R.attr.colorPrimaryFixedDim);
+ sCustomIconColorMap.put(
+ Resources.getSystem().getIdentifier("ic_doc_generic", "drawable", "android"),
+ com.google.android.material.R.attr.colorOutline);
+ sCustomIconColorMap.put(
+ Resources.getSystem()
+ .getIdentifier("ic_doc_certificate", "drawable", "android"),
+ com.google.android.material.R.attr.colorOutline);
+ sCustomIconColorMap.put(
+ Resources.getSystem().getIdentifier("ic_doc_codes", "drawable", "android"),
+ com.google.android.material.R.attr.colorOutline);
+ sCustomIconColorMap.put(
+ Resources.getSystem().getIdentifier("ic_doc_contact", "drawable", "android"),
+ com.google.android.material.R.attr.colorOutline);
+ sCustomIconColorMap.put(
+ Resources.getSystem().getIdentifier("ic_doc_font", "drawable", "android"),
+ com.google.android.material.R.attr.colorOutline);
+ }
+ }
+
public static Drawable loadPackageIcon(Context context, UserId userId, String authority,
int icon, boolean maybeShowBadge) {
if (icon != 0) {
@@ -61,7 +102,17 @@ public class IconUtils {
*/
public static Drawable loadMimeIcon(Context context, String mimeType) {
if (mimeType == null) return null;
- return context.getContentResolver().getTypeInfo(mimeType).getIcon().loadDrawable(context);
+ Icon icon = context.getContentResolver().getTypeInfo(mimeType).getIcon();
+ Drawable drawable = icon.loadDrawable(context);
+ // TODO(b/400263417): Remove this once RRO mime icons support dynamic colors.
+ if (isUseMaterial3FlagEnabled()
+ && drawable != null
+ && sCustomIconColorMap.containsKey(icon.getResId())) {
+ drawable.setTint(
+ ColorUtils.resolveMaterialColorAttribute(
+ context, sCustomIconColorMap.get(icon.getResId())));
+ }
+ return drawable;
}
public static Drawable applyTintColor(Context context, int drawableId, int tintColorId) {
@@ -80,4 +131,32 @@ public class IconUtils {
context.getTheme().resolveAttribute(tintAttrId, outValue, true);
return applyTintColor(context, drawableId, outValue.resourceId);
}
+
+ /**
+ * When a ImageView loads a thumbnail from a bitmap, we usually uses a CardView to wrap it to
+ * apply CardView's corner radius to the ImageView. This causes the corner pixelation of the
+ * thumbnail especially when there's a border (stroke) around the CardView. This method creates
+ * a custom clip outline with the correct shape to fix this issue.
+ *
+ * @param imageView ImageView to apply clip outline.
+ * @param strokeWidth stroke width of the thumbnail.
+ * @param cornerRadius corner radius of the thumbnail.
+ */
+ public static void applyThumbnailClipOutline(
+ ImageView imageView, int strokeWidth, int cornerRadius) {
+ ViewOutlineProvider outlineProvider =
+ new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRoundRect(
+ strokeWidth,
+ strokeWidth,
+ view.getWidth() - strokeWidth,
+ view.getHeight() - strokeWidth,
+ cornerRadius);
+ }
+ };
+ imageView.setOutlineProvider(outlineProvider);
+ imageView.setClipToOutline(true);
+ }
}
diff --git a/src/com/android/documentsui/JobPanelController.kt b/src/com/android/documentsui/JobPanelController.kt
new file mode 100644
index 000000000..b3b5f1cfd
--- /dev/null
+++ b/src/com/android/documentsui/JobPanelController.kt
@@ -0,0 +1,126 @@
+/*
+ * 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
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.util.Log
+import android.view.MenuItem
+import android.widget.ProgressBar
+import com.android.documentsui.base.Menus
+import com.android.documentsui.services.FileOperationService
+import com.android.documentsui.services.FileOperationService.EXTRA_PROGRESS
+import com.android.documentsui.services.Job
+import com.android.documentsui.services.JobProgress
+
+/**
+ * JobPanelController is responsible for receiving broadcast updates from the [FileOperationService]
+ * and updating a given menu item to reflect the current progress.
+ */
+class JobPanelController(private val mContext: Context) : BroadcastReceiver() {
+ companion object {
+ private const val TAG = "JobPanelController"
+ private const val MAX_PROGRESS = 100
+ }
+
+ private enum class State {
+ INVISIBLE, INDETERMINATE, VISIBLE
+ }
+
+ /** The current state of the menu progress item. */
+ private var mState = State.INVISIBLE
+
+ /** The total progress from 0 to MAX_PROGRESS. */
+ private var mTotalProgress = 0
+
+ /** List of jobs currently tracked by this class. */
+ private val mCurrentJobs = LinkedHashMap<String, JobProgress>()
+
+ /** Current menu item being controlled by this class. */
+ private var mMenuItem: MenuItem? = null
+
+ init {
+ val filter = IntentFilter(FileOperationService.ACTION_PROGRESS)
+ mContext.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED)
+ }
+
+ private fun updateMenuItem(animate: Boolean) {
+ mMenuItem?.let {
+ Menus.setEnabledAndVisible(it, mState != State.INVISIBLE)
+ val icon = it.actionView as ProgressBar
+ when (mState) {
+ State.INDETERMINATE -> icon.isIndeterminate = true
+ State.VISIBLE -> icon.apply {
+ isIndeterminate = false
+ setProgress(mTotalProgress, animate)
+ }
+ State.INVISIBLE -> {}
+ }
+ }
+ }
+
+ /**
+ * Sets the menu item controlled by this class. The item's actionView must be a [ProgressBar].
+ */
+ fun setMenuItem(menuItem: MenuItem) {
+ (menuItem.actionView as ProgressBar).max = MAX_PROGRESS
+ mMenuItem = menuItem
+ updateMenuItem(animate = false)
+ }
+
+ override fun onReceive(context: Context?, intent: Intent) {
+ val progresses = intent.getParcelableArrayListExtra<JobProgress>(
+ EXTRA_PROGRESS,
+ JobProgress::class.java
+ )
+ updateProgress(progresses!!)
+ }
+
+ private fun updateProgress(progresses: List<JobProgress>) {
+ var requiredBytes = 0L
+ var currentBytes = 0L
+ var allFinished = true
+
+ for (jobProgress in progresses) {
+ Log.d(TAG, "Received $jobProgress")
+ mCurrentJobs.put(jobProgress.id, jobProgress)
+ }
+ for (jobProgress in mCurrentJobs.values) {
+ if (jobProgress.state != Job.STATE_COMPLETED) {
+ allFinished = false
+ }
+ if (jobProgress.requiredBytes != -1L && jobProgress.currentBytes != -1L) {
+ requiredBytes += jobProgress.requiredBytes
+ currentBytes += jobProgress.currentBytes
+ }
+ }
+
+ if (mCurrentJobs.isEmpty()) {
+ mState = State.INVISIBLE
+ } else if (requiredBytes != 0L) {
+ mState = State.VISIBLE
+ mTotalProgress = (MAX_PROGRESS * currentBytes / requiredBytes).toInt()
+ } else if (allFinished) {
+ mState = State.VISIBLE
+ mTotalProgress = MAX_PROGRESS
+ } else {
+ mState = State.INDETERMINATE
+ }
+ updateMenuItem(animate = true)
+ }
+}
diff --git a/src/com/android/documentsui/MenuManager.java b/src/com/android/documentsui/MenuManager.java
index 5f17d7e02..a46659c77 100644
--- a/src/com/android/documentsui/MenuManager.java
+++ b/src/com/android/documentsui/MenuManager.java
@@ -25,6 +25,7 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
@@ -158,11 +159,11 @@ public abstract class MenuManager {
}
/**
- * @see DirectoryFragment#onCreateContextMenu
- *
* Called when user tries to generate a context menu anchored to a file when the selection
* doesn't contain any folder.
*
+ * @see DirectoryFragment#onCreateContextMenu
+ *
* @param selectionDetails
* containsFiles may return false because this may be called when user right clicks on an
* unselectable item in pickers
@@ -183,15 +184,20 @@ public abstract class MenuManager {
updateRename(rename, selectionDetails);
updateViewInOwner(viewInOwner, selectionDetails);
+ if (isZipNgFlagEnabled()) {
+ updateExtractHere(menu.findItem(R.id.dir_menu_extract_here), selectionDetails);
+ updateBrowse(menu.findItem(R.id.dir_menu_browse), selectionDetails);
+ }
+
updateContextMenu(menu, selectionDetails);
}
/**
- * @see DirectoryFragment#onCreateContextMenu
- *
* Called when user tries to generate a context menu anchored to a folder when the selection
* doesn't contain any file.
*
+ * @see DirectoryFragment#onCreateContextMenu
+ *
* @param selectionDetails
* containDirectories may return false because this may be called when user right clicks on
* an unselectable item in pickers
@@ -276,6 +282,15 @@ public abstract class MenuManager {
public abstract void updateKeyboardShortcutsMenu(
List<KeyboardShortcutGroup> data, IntFunction<String> stringSupplier);
+ /**
+ * Called on option menu creation to instantiate the job progress item if applicable.
+ *
+ * @param menu The option menu created.
+ */
+ public void instantiateJobProgress(Menu menu) {
+ // This icon is not shown in the picker.
+ }
+
protected void updateModePicker(MenuItem grid, MenuItem list) {
// The order of enabling disabling menu item in wrong order removed accessibility focus.
if (mState.derivedMode != State.MODE_LIST) {
@@ -383,6 +398,14 @@ public abstract class MenuManager {
Menus.setEnabledAndVisible(extractTo, false);
}
+ protected void updateExtractHere(@NonNull MenuItem it, @NonNull SelectionDetails selection) {
+ Menus.setEnabledAndVisible(it, false);
+ }
+
+ protected void updateBrowse(@NonNull MenuItem it, @NonNull SelectionDetails selection) {
+ Menus.setEnabledAndVisible(it, false);
+ }
+
protected void updatePasteInto(MenuItem pasteInto, SelectionDetails selectionDetails) {
Menus.setEnabledAndVisible(pasteInto, false);
}
@@ -404,25 +427,42 @@ public abstract class MenuManager {
}
protected abstract void updateSelectAll(MenuItem selectAll);
+
protected abstract void updateSelectAll(MenuItem selectAll, SelectionDetails selectionDetails);
+
protected abstract void updateDeselectAll(
MenuItem deselectAll, SelectionDetails selectionDetails);
+
protected abstract void updateCreateDir(MenuItem createDir);
/**
* Access to meta data about the selection.
*/
public interface SelectionDetails {
+ /** Gets the total number of items (files and directories) in the selection. */
+ int size();
+
+ /** Returns whether the selection contains at least a directory. */
boolean containsDirectories();
+ /** Returns whether the selection contains at least a file. */
boolean containsFiles();
- int size();
-
+ /**
+ * Returns whether the selection contains at least a file that has not been fully downloaded
+ * yet.
+ */
boolean containsPartialFiles();
+ /** Returns whether the selection contains at least a file located in a mounted archive. */
boolean containsFilesInArchive();
+ /**
+ * Returns whether the selection contains exactly one file which is also a supported archive
+ * type.
+ */
+ boolean isArchive();
+
// TODO: Update these to express characteristics instead of answering concrete questions,
// since the answer to those questions is (or can be) activity specific.
boolean canDelete();
diff --git a/src/com/android/documentsui/NavigationViewManager.java b/src/com/android/documentsui/NavigationViewManager.java
index 86b5e517f..bbae22d42 100644
--- a/src/com/android/documentsui/NavigationViewManager.java
+++ b/src/com/android/documentsui/NavigationViewManager.java
@@ -18,6 +18,7 @@ package com.android.documentsui;
import static com.android.documentsui.base.SharedMinimal.VERBOSE;
import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled;
+import static com.android.documentsui.util.FlagUtils.isVisualSignalsFlagEnabled;
import android.content.res.Resources;
import android.content.res.TypedArray;
@@ -27,10 +28,10 @@ import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
+import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.Window;
import android.view.WindowManager;
-import android.widget.FrameLayout;
import androidx.annotation.ColorRes;
import androidx.annotation.Nullable;
@@ -144,7 +145,13 @@ public class NavigationViewManager extends SelectionTracker.SelectionObserver<St
mState = state;
mEnv = env;
mBreadcrumb = breadcrumb;
- mBreadcrumb.setup(env, state, this::onNavigationItemSelected);
+ mBreadcrumb.setup(
+ env,
+ state,
+ this::onNavigationItemSelected,
+ isUseMaterial3FlagEnabled()
+ ? activity.findViewById(R.id.breadcrumb_top_divider)
+ : null);
mConfigStore = configStore;
mInjector = injector;
mProfileTabs =
@@ -297,7 +304,10 @@ public class NavigationViewManager extends SelectionTracker.SelectionObserver<St
}
public void update() {
- updateScrollFlag();
+ // If use_material3 flag is ON, we don't want any scroll behavior, thus skipping this logic.
+ if (!isUseMaterial3FlagEnabled()) {
+ updateScrollFlag();
+ }
updateToolbar();
mProfileTabs.updateView();
@@ -400,6 +410,9 @@ public class NavigationViewManager extends SelectionTracker.SelectionObserver<St
mActivity.getResources().getBoolean(R.bool.full_bar_search_view);
boolean showSearchBar = mActivity.getResources().getBoolean(R.bool.show_search_bar);
mInjector.searchManager.install(mToolbar.getMenu(), fullBarSearch, showSearchBar);
+ if (isVisualSignalsFlagEnabled()) {
+ mInjector.menuManager.instantiateJobProgress(mToolbar.getMenu());
+ }
}
mInjector.menuManager.updateOptionMenu(mToolbar.getMenu());
mInjector.searchManager.showMenu(mState.stack);
@@ -467,8 +480,11 @@ public class NavigationViewManager extends SelectionTracker.SelectionObserver<St
}
if (!mIsActionModeActivated) {
- FrameLayout.LayoutParams headerLayoutParams =
- (FrameLayout.LayoutParams) mHeader.getLayoutParams();
+ // This could be either FrameLayout.LayoutParams (when use_material3 flag is OFF) or
+ // LinearLayout.LayoutParams (when use_material3 flag is ON), so use the common parent
+ // class instead to make it work for both scenarios.
+ ViewGroup.MarginLayoutParams headerLayoutParams =
+ (ViewGroup.MarginLayoutParams) mHeader.getLayoutParams();
headerLayoutParams.setMargins(0, /* top= */ headerTopOffset, 0, 0);
mHeader.setLayoutParams(headerLayoutParams);
}
@@ -498,7 +514,7 @@ public class NavigationViewManager extends SelectionTracker.SelectionObserver<St
}
interface Breadcrumb {
- void setup(Environment env, State state, IntConsumer listener);
+ void setup(Environment env, State state, IntConsumer listener, @Nullable View topDivider);
void show(boolean visibility);
diff --git a/src/com/android/documentsui/ProfileTabs.java b/src/com/android/documentsui/ProfileTabs.java
index 5aacc22b0..74db6f4bd 100644
--- a/src/com/android/documentsui/ProfileTabs.java
+++ b/src/com/android/documentsui/ProfileTabs.java
@@ -157,9 +157,12 @@ public class ProfileTabs implements ProfileTabsAddons {
int tabMarginSide = (int) mTabsContainer.getContext().getResources()
.getDimension(R.dimen.profile_tab_margin_side);
if (isUseMaterial3FlagEnabled()) {
- // M3 uses the margin value as the right margin, except for the last child.
+ final boolean isRtl = mTabs.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ // if use_material3 flag is ON, we uses the margin value as the right margin
+ // (left margin for RTL), except for the last child.
if (i != mTabs.getTabCount() - 1) {
- marginLayoutParams.setMargins(0, 0, tabMarginSide, 0);
+ marginLayoutParams.setMargins(
+ isRtl ? tabMarginSide : 0, 0, isRtl ? 0 : tabMarginSide, 0);
}
} else {
marginLayoutParams.setMargins(tabMarginSide, 0, tabMarginSide, 0);
diff --git a/src/com/android/documentsui/UserManagerState.java b/src/com/android/documentsui/UserManagerState.java
index d2ddae615..1023c8c1d 100644
--- a/src/com/android/documentsui/UserManagerState.java
+++ b/src/com/android/documentsui/UserManagerState.java
@@ -22,7 +22,6 @@ import static com.android.documentsui.DevicePolicyResources.Drawables.Style.SOLI
import static com.android.documentsui.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
import static com.android.documentsui.DevicePolicyResources.Strings.PERSONAL_TAB;
import static com.android.documentsui.DevicePolicyResources.Strings.WORK_TAB;
-import static com.android.documentsui.base.SharedMinimal.DEBUG;
import android.Manifest;
import android.annotation.SuppressLint;
@@ -263,35 +262,13 @@ public interface UserManagerState {
}
synchronized (mCanForwardToProfileIdMap) {
if (!mCanForwardToProfileIdMap.containsKey(userId)) {
-
- UserHandle handle = UserHandle.of(userId.getIdentifier());
-
- // Decide if to use the parent's access, or this handle's access.
- if (isCrossProfileContentSharingStrategyDelegatedFromParent(handle)) {
- UserHandle parentHandle = mUserManager.getProfileParent(handle);
- // Couldn't resolve parent to check access, so fail closed.
- if (parentHandle == null) {
- mCanForwardToProfileIdMap.put(userId, false);
- } else if (mCurrentUser.getIdentifier()
- == parentHandle.getIdentifier()) {
- // Check if the parent is the current user, if so this profile
- // is also accessible.
- mCanForwardToProfileIdMap.put(userId, true);
-
- } else {
- UserId parent = UserId.of(parentHandle);
- mCanForwardToProfileIdMap.put(
- userId,
- doesCrossProfileForwardingActivityExistForUser(
- mCurrentStateIntent, parent));
- }
- } else {
- // Update the profile map for this profile.
- mCanForwardToProfileIdMap.put(
- userId,
- doesCrossProfileForwardingActivityExistForUser(
- mCurrentStateIntent, userId));
- }
+ mCanForwardToProfileIdMap.put(
+ userId,
+ isCrossProfileAllowedToUser(
+ mContext,
+ mCurrentStateIntent,
+ UserId.CURRENT_USER,
+ userId));
}
}
} else {
@@ -331,43 +308,37 @@ public interface UserManagerState {
if (mUserManager == null) {
Log.e(TAG, "cannot obtain user manager");
- result.add(mCurrentUser);
return result;
}
final List<UserHandle> userProfiles = mUserManager.getUserProfiles();
- if (userProfiles.size() < 2) {
- result.add(mCurrentUser);
- return result;
- }
- if (SdkLevel.isAtLeastV()) {
- getUserIdsInternalPostV(userProfiles, result);
- } else {
- getUserIdsInternalPreV(userProfiles, result);
- }
- return result;
- }
+ result.add(mCurrentUser);
+ boolean currentUserIsManaged =
+ mUserManager.isManagedProfile(mCurrentUser.getIdentifier());
- @SuppressLint("NewApi")
- private void getUserIdsInternalPostV(List<UserHandle> userProfiles, List<UserId> result) {
- for (UserHandle userHandle : userProfiles) {
- if (userHandle.getIdentifier() == ActivityManager.getCurrentUser()) {
- result.add(UserId.of(userHandle));
+ for (UserHandle handle : userProfiles) {
+ if (SdkLevel.isAtLeastV()) {
+ if (!isProfileAllowed(handle)) {
+ continue;
+ }
} else {
- // Out of all the profiles returned by user manager the profiles that are
- // returned should satisfy both the following conditions:
- // 1. It has user property SHOW_IN_SHARING_SURFACES_SEPARATE
- // 2. Quite mode is not enabled, if it is enabled then the profile's user
- // property is not SHOW_IN_QUIET_MODE_HIDDEN
- if (isProfileAllowed(userHandle)) {
- result.add(UserId.of(userHandle));
+ // Only allow managed profiles + the parent user on lower than V.
+ if (currentUserIsManaged
+ && mUserManager.getProfileParent(mCurrentUser.getUserHandle())
+ == handle) {
+ // Intentionally empty so that this profile gets added.
+ } else if (!mUserManager.isManagedProfile(handle.getIdentifier())) {
+ continue;
}
}
+
+ // Ensure the system user doesn't get added twice.
+ if (result.contains(UserId.of(handle))) continue;
+ result.add(UserId.of(handle));
}
- if (result.isEmpty()) {
- result.add(mCurrentUser);
- }
+
+ return result;
}
/**
@@ -444,33 +415,6 @@ public interface UserManagerState {
return false;
}
- private void getUserIdsInternalPreV(List<UserHandle> userProfiles, List<UserId> result) {
- result.add(mCurrentUser);
- UserId systemUser = null;
- UserId managedUser = null;
- for (UserHandle userHandle : userProfiles) {
- if (userHandle.isSystem()) {
- systemUser = UserId.of(userHandle);
- } else if (mUserManager.isManagedProfile(userHandle.getIdentifier())) {
- managedUser = UserId.of(userHandle);
- }
- }
- if (mCurrentUser.isSystem() && managedUser != null) {
- result.add(managedUser);
- } else if (mCurrentUser.isManagedProfile(mUserManager) && systemUser != null) {
- result.add(0, systemUser);
- } else {
- if (DEBUG) {
- Log.w(
- TAG,
- "The current user "
- + UserId.CURRENT_USER
- + " is neither system nor managed user. has system user: "
- + (systemUser != null));
- }
- }
- }
-
private void getUserIdToLabelMapInternal() {
if (SdkLevel.isAtLeastV()) {
getUserIdToLabelMapInternalPostV();
@@ -651,50 +595,124 @@ public interface UserManagerState {
*/
private void getCanForwardToProfileIdMapInternal(Intent intent) {
- Map<UserId, Boolean> profileIsAccessibleToProcessOwner = new HashMap<>();
+ synchronized (mCanForwardToProfileIdMap) {
+ mCanForwardToProfileIdMap.clear();
+ for (UserId userId : getUserIds()) {
+ mCanForwardToProfileIdMap.put(
+ userId,
+ isCrossProfileAllowedToUser(
+ mContext, intent, mCurrentUser, userId));
+ }
+ }
+ }
- List<UserId> delegatedFromParent = new ArrayList<>();
+ /**
+ * Determines if the provided UserIds support CrossProfile content sharing.
+ *
+ * <p>This method accepts a pair of user handles (from/to) and determines if CrossProfile
+ * access is permitted between those two profiles.
+ *
+ * <p>There are differences is on how the access is determined based on the platform SDK:
+ *
+ * <p>For Platform SDK < V:
+ *
+ * <p>A check for CrossProfileIntentForwarders in the origin (from) profile that target the
+ * destination (to) profile. If such a forwarder exists, then access is allowed, and denied
+ * otherwise.
+ *
+ * <p>For Platform SDK >= V:
+ *
+ * <p>The method now takes into account access delegation, which was first added in Android
+ * V.
+ *
+ * <p>For profiles that set the [CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT]
+ * property in its [UserProperties], its parent profile will be substituted in for its side
+ * of the check.
+ *
+ * <p>ex. For access checks between a Managed (from) and Private (to) profile, where: -
+ * Managed does not delegate to its parent - Private delegates to its parent
+ *
+ * <p>The following logic is performed: Managed -> parent(Private)
+ *
+ * <p>The same check in the other direction would yield: parent(Private) -> Managed
+ *
+ * <p>Note how the private profile is never actually used for either side of the check,
+ * since it is delegating its access check to the parent. And thus, if Managed can access
+ * the parent, it can also access the private.
+ *
+ * @param context Current context object, for switching user contexts.
+ * @param intent The current intent the Photopicker is running under.
+ * @param fromUser The Origin profile, where the user is coming from
+ * @param toUser The destination profile, where the user is attempting to go to.
+ * @return Whether CrossProfile content sharing is supported in this handle.
+ */
+ private boolean isCrossProfileAllowedToUser(
+ Context context, Intent intent, UserId fromUser, UserId toUser) {
- for (UserId userId : getUserIds()) {
+ // Early exit conditions, accessing self.
+ // NOTE: It is also possible to reach this state if this method is recursively checking
+ // from: parent(A) to:parent(B) where A and B are both children of the same parent.
+ if (fromUser.getIdentifier() == toUser.getIdentifier()) {
+ return true;
+ }
- // Early exit, self is always accessible.
- if (userId.getIdentifier() == mCurrentUser.getIdentifier()) {
- profileIsAccessibleToProcessOwner.put(userId, true);
- continue;
- }
+ // Decide if we should use actual from or parent(from)
+ UserHandle currentFromUser =
+ getProfileToCheckCrossProfileAccess(fromUser.getUserHandle());
- // CrossProfileContentSharingStrategyDelegatedFromParent is only V+ sdks.
- if (SdkLevel.isAtLeastV()
- && isCrossProfileContentSharingStrategyDelegatedFromParent(
- UserHandle.of(userId.getIdentifier()))) {
- delegatedFromParent.add(userId);
- continue;
- }
+ // Decide if we should use actual to or parent(to)
+ UserHandle currentToUser = getProfileToCheckCrossProfileAccess(toUser.getUserHandle());
- // Check for cross profile & add to the map.
- profileIsAccessibleToProcessOwner.put(
- userId, doesCrossProfileForwardingActivityExistForUser(intent, userId));
+ // When the from/to has changed from the original parameters, recursively restart the
+ // checks with the new from/to handles.
+ if (fromUser.getIdentifier() != currentFromUser.getIdentifier()
+ || toUser.getIdentifier() != currentToUser.getIdentifier()) {
+ return isCrossProfileAllowedToUser(
+ context, intent, UserId.of(currentFromUser), UserId.of(currentToUser));
}
- // For profiles that delegate their access to the parent, set the access for
- // those profiles
- // equal to the same as their parent.
- for (UserId userId : delegatedFromParent) {
- UserHandle parent =
- mUserManager.getProfileParent(UserHandle.of(userId.getIdentifier()));
- profileIsAccessibleToProcessOwner.put(
- userId,
- profileIsAccessibleToProcessOwner.getOrDefault(
- UserId.of(parent), /* default= */ false));
- }
+ PackageManager pm = context.getPackageManager();
+ return doesCrossProfileIntentForwarderExist(intent, pm, fromUser, toUser);
+ }
- synchronized (mCanForwardToProfileIdMap) {
- mCanForwardToProfileIdMap.clear();
- for (Map.Entry<UserId, Boolean> entry :
- profileIsAccessibleToProcessOwner.entrySet()) {
- mCanForwardToProfileIdMap.put(entry.getKey(), entry.getValue());
+ /**
+ * Determines if the target UserHandle delegates its content sharing to its parent.
+ *
+ * @param userHandle The target handle to check delegation for.
+ * @return TRUE if V+ and the handle delegates to parent. False otherwise.
+ */
+ private boolean isCrossProfileStrategyDelegatedToParent(UserHandle userHandle) {
+ if (SdkLevel.isAtLeastV()) {
+ if (mUserManager == null) {
+ Log.e(TAG, "Cannot obtain user manager");
+ return false;
+ }
+ UserProperties userProperties = mUserManager.getUserProperties(userHandle);
+ if (userProperties.getCrossProfileContentSharingStrategy()
+ == userProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT) {
+ return true;
}
}
+ return false;
+ }
+
+ /**
+ * Acquires the correct {@link UserHandle} which should be used for CrossProfile access
+ * checks.
+ *
+ * @param userHandle the origin handle.
+ * @return The UserHandle that should be used for cross profile access checks. In the event
+ * the origin handle delegates its access, this may not be the same handle as the origin
+ * handle.
+ */
+ private UserHandle getProfileToCheckCrossProfileAccess(UserHandle userHandle) {
+ if (mUserManager == null) {
+ Log.e(TAG, "Cannot obtain user manager");
+ return null;
+ }
+ return isCrossProfileStrategyDelegatedToParent(userHandle)
+ ? mUserManager.getProfileParent(userHandle)
+ : userHandle;
}
/**
@@ -706,16 +724,18 @@ public interface UserManagerState {
* @return whether a CrossProfileIntentForwardingActivity could be found for the given
* intent, and user.
*/
- private boolean doesCrossProfileForwardingActivityExistForUser(
- Intent intent, UserId targetUserId) {
+ private boolean doesCrossProfileIntentForwarderExist(
+ Intent intent, PackageManager pm, UserId fromUser, UserId targetUserId) {
- final PackageManager pm = mContext.getPackageManager();
final Intent intentToCheck = (Intent) intent.clone();
intentToCheck.setComponent(null);
intentToCheck.setPackage(null);
for (ResolveInfo resolveInfo :
- pm.queryIntentActivities(intentToCheck, PackageManager.MATCH_DEFAULT_ONLY)) {
+ pm.queryIntentActivitiesAsUser(
+ intentToCheck,
+ PackageManager.MATCH_DEFAULT_ONLY,
+ fromUser.getUserHandle())) {
if (resolveInfo.isCrossProfileIntentForwarderActivity()) {
/*
diff --git a/src/com/android/documentsui/archives/ArchiveRegistry.java b/src/com/android/documentsui/archives/ArchiveRegistry.java
index 91e0e20f5..3417e45ad 100644
--- a/src/com/android/documentsui/archives/ArchiveRegistry.java
+++ b/src/com/android/documentsui/archives/ArchiveRegistry.java
@@ -24,13 +24,12 @@ import static org.apache.commons.compress.compressors.CompressorStreamFactory.XZ
import androidx.annotation.Nullable;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
import org.apache.commons.compress.compressors.brotli.BrotliUtils;
import org.apache.commons.compress.compressors.xz.XZUtils;
+import java.util.HashMap;
+import java.util.Map;
+
/**
* To query how to generate ArchiveHandle, how to create CompressInputStream and how to create
* ArchiveInputStream by using MIME type in ArchiveRegistry.
@@ -136,8 +135,4 @@ final class ArchiveRegistry {
static Integer getArchiveType(String mimeType) {
return sHandleArchiveMap.get(mimeType);
}
-
- static Set<String> getSupportList() {
- return sHandleArchiveMap.keySet();
- }
}
diff --git a/src/com/android/documentsui/archives/ArchivesProvider.java b/src/com/android/documentsui/archives/ArchivesProvider.java
index 3406cd708..dd221f416 100644
--- a/src/com/android/documentsui/archives/ArchivesProvider.java
+++ b/src/com/android/documentsui/archives/ArchivesProvider.java
@@ -44,7 +44,6 @@ import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
-import java.util.Set;
/**
* Provides basic implementation for creating, extracting and accessing
@@ -62,7 +61,6 @@ public class ArchivesProvider extends DocumentsProvider {
private static final String TAG = "ArchivesProvider";
private static final String METHOD_ACQUIRE_ARCHIVE = "acquireArchive";
private static final String METHOD_RELEASE_ARCHIVE = "releaseArchive";
- private static final Set<String> ZIP_MIME_TYPES = ArchiveRegistry.getSupportList();
@GuardedBy("mArchives")
private final Map<Key, Loader> mArchives = new HashMap<>();
@@ -235,16 +233,9 @@ public class ArchivesProvider extends DocumentsProvider {
return loader.get().openDocumentThumbnail(documentId, sizeHint, signal);
}
- /**
- * Returns true if the passed mime type is supported by the helper.
- */
+ /** Returns whether the given mime type is a supported archive type. */
public static boolean isSupportedArchiveType(String mimeType) {
- for (final String zipMimeType : ZIP_MIME_TYPES) {
- if (zipMimeType.equals(mimeType)) {
- return true;
- }
- }
- return false;
+ return ArchiveRegistry.getArchiveType(mimeType) != null;
}
/**
diff --git a/src/com/android/documentsui/base/DocumentInfo.java b/src/com/android/documentsui/base/DocumentInfo.java
index ad09c45cf..6306f14f1 100644
--- a/src/com/android/documentsui/base/DocumentInfo.java
+++ b/src/com/android/documentsui/base/DocumentInfo.java
@@ -17,6 +17,7 @@
package com.android.documentsui.base;
import static com.android.documentsui.base.SharedMinimal.DEBUG;
+import static com.android.documentsui.util.FlagUtils.isZipNgFlagEnabled;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
@@ -319,7 +320,8 @@ public class DocumentInfo implements Durable, Parcelable {
// Containers are documents which can be opened in DocumentsUI as folders.
public boolean isContainer() {
- return isDirectory() || (isArchive() && !isInArchive() && !isPartial());
+ return isDirectory() || (isArchive() && !isPartial() && (isZipNgFlagEnabled()
+ || !isInArchive()));
}
public boolean isVirtual() {
@@ -342,7 +344,6 @@ public class DocumentInfo implements Durable, Parcelable {
return userId.buildDocumentUriAsUser(authority, documentId);
}
-
/**
* Returns a tree document uri representing this {@link DocumentInfo}. The URI may contain user
* information. Use this when uri is needed externally.
diff --git a/src/com/android/documentsui/base/UserId.java b/src/com/android/documentsui/base/UserId.java
index 21842917a..7aff61e9b 100644
--- a/src/com/android/documentsui/base/UserId.java
+++ b/src/com/android/documentsui/base/UserId.java
@@ -95,6 +95,13 @@ public final class UserId {
}
/**
+ * Return this User's {@link UserHandle}.
+ */
+ public UserHandle getUserHandle() {
+ return mUserHandle;
+ }
+
+ /**
* Return a package manager instance of this user.
*/
public PackageManager getPackageManager(Context context) {
diff --git a/src/com/android/documentsui/dirlist/DirectoryAddonsAdapter.java b/src/com/android/documentsui/dirlist/DirectoryAddonsAdapter.java
index 8989853a0..8be400924 100644
--- a/src/com/android/documentsui/dirlist/DirectoryAddonsAdapter.java
+++ b/src/com/android/documentsui/dirlist/DirectoryAddonsAdapter.java
@@ -16,6 +16,8 @@
package com.android.documentsui.dirlist;
+import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled;
+
import android.os.UserManager;
import android.view.ViewGroup;
@@ -207,6 +209,11 @@ final class DirectoryAddonsAdapter extends DocumentsAdapter {
return;
}
+ if (isUseMaterial3FlagEnabled()) {
+ // Do not add a visual break between folders and documents in Material3.
+ return;
+ }
+
// Walk down the list of IDs till we encounter something that's not a directory, and
// insert a whitespace element - this introduces a visual break in the grid between
// folders and documents.
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 855a8273d..2ea906a60 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -23,6 +23,7 @@ import static com.android.documentsui.base.State.MODE_GRID;
import static com.android.documentsui.base.State.MODE_LIST;
import static com.android.documentsui.util.FlagUtils.isDesktopFileHandlingFlagEnabled;
import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled;
+import static com.android.documentsui.util.FlagUtils.isZipNgFlagEnabled;
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
@@ -827,6 +828,13 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
}
private int getSaveLayoutHeight() {
+ // When use_material3 flag is on, the bottom section not only includes the container_save,
+ // but also includes the breadcrumb and the divider, so we need to use the total height
+ // for their parent container.
+ if (isUseMaterial3FlagEnabled()) {
+ View bottomSection = getActivity().findViewById(R.id.bottom_container);
+ return bottomSection == null ? 0 : bottomSection.getHeight();
+ }
View containerSave = getActivity().findViewById(R.id.container_save);
return containerSave == null ? 0 : containerSave.getHeight();
}
@@ -939,11 +947,13 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
mSelectionMgr.copySelection(selection);
final int id = item.getItemId();
- if (isDesktopFileHandlingFlagEnabled() && id == R.id.dir_menu_open) {
- // On desktop, "open" is displayed in file management mode (i.e. `files.MenuManager`).
- // This menu item behaves the same as double click on the menu item which is handled by
- // onItemActivated but since onItemActivated requires a RecylcerView ItemDetails, we're
- // using viewDocument that takes a Selection.
+ if ((isDesktopFileHandlingFlagEnabled() && id == R.id.dir_menu_open)
+ || (isZipNgFlagEnabled() && id == R.id.dir_menu_browse)) {
+ // The "Open" menu item is displayed in desktop mode.
+ // The "Browse" menu item is displayed for supported archives in advanced ZIP mode.
+ // These menu items behave the same as a double click on the matching document which
+ // is handled by onItemActivated but since onItemActivated requires a RecyclerView
+ // ItemDetails, we're using viewDocument that takes a Selection.
viewDocument(selection);
return true;
} else if (id == R.id.action_menu_select || id == R.id.dir_menu_open) {
diff --git a/src/com/android/documentsui/dirlist/DocumentHolder.java b/src/com/android/documentsui/dirlist/DocumentHolder.java
index 8e5f50636..957975c4b 100644
--- a/src/com/android/documentsui/dirlist/DocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/DocumentHolder.java
@@ -58,8 +58,6 @@ public abstract class DocumentHolder
static final float DISABLED_ALPHA = isUseMaterial3FlagEnabled() ? 0.6f : 0.3f;
- static final int THUMBNAIL_STROKE_WIDTH = isUseMaterial3FlagEnabled() ? 2 : 0;
-
protected final Context mContext;
protected @Nullable String mModelId;
diff --git a/src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java b/src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java
index 838b1fa72..64409673e 100644
--- a/src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java
+++ b/src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java
@@ -22,13 +22,13 @@ import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
-import android.util.TypedValue;
import android.view.MotionEvent;
import androidx.annotation.ColorRes;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.android.documentsui.R;
+import com.android.documentsui.util.ColorUtils;
/**
* A {@link SwipeRefreshLayout} that does not intercept any touch events. This relies on its nested
@@ -46,20 +46,12 @@ public class DocumentsSwipeRefreshLayout extends SwipeRefreshLayout {
super(context, attrs);
if (isUseMaterial3FlagEnabled()) {
- TypedValue spinnerColor = new TypedValue();
- context.getTheme()
- .resolveAttribute(
- com.google.android.material.R.attr.colorOnPrimaryContainer,
- spinnerColor,
- true);
- setColorSchemeResources(spinnerColor.resourceId);
- TypedValue spinnerBackgroundColor = new TypedValue();
- context.getTheme()
- .resolveAttribute(
- com.google.android.material.R.attr.colorPrimaryContainer,
- spinnerBackgroundColor,
- true);
- setProgressBackgroundColorSchemeResource(spinnerBackgroundColor.resourceId);
+ setColorSchemeColors(
+ ColorUtils.resolveMaterialColorAttribute(
+ context, com.google.android.material.R.attr.colorOnPrimaryContainer));
+ setProgressBackgroundColorSchemeColor(
+ ColorUtils.resolveMaterialColorAttribute(
+ context, com.google.android.material.R.attr.colorPrimaryContainer));
} else {
final int[] styledAttrs = {android.R.attr.colorAccent};
diff --git a/src/com/android/documentsui/dirlist/GridDocumentHolder.java b/src/com/android/documentsui/dirlist/GridDocumentHolder.java
index f2802ff66..703f870e2 100644
--- a/src/com/android/documentsui/dirlist/GridDocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/GridDocumentHolder.java
@@ -41,6 +41,7 @@ import androidx.annotation.RequiresApi;
import com.android.documentsui.ConfigStore;
import com.android.documentsui.DocumentsApplication;
+import com.android.documentsui.IconUtils;
import com.android.documentsui.R;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.Shared;
@@ -79,6 +80,8 @@ final class GridDocumentHolder extends DocumentHolder {
// Non-null only when useMaterial3 flag is ON.
private final @Nullable MaterialCardView mIconWrapper;
+ // It will be 0 when use_material flag is OFF.
+ private final int mThumbnailStrokeWidth;
GridDocumentHolder(Context context, ViewGroup parent, IconHelper iconHelper,
ConfigStore configStore) {
@@ -90,12 +93,16 @@ final class GridDocumentHolder extends DocumentHolder {
mIconLayout = null;
mIconMimeSm = null;
mIconCheck = null;
+ mThumbnailStrokeWidth =
+ context.getResources()
+ .getDimensionPixelSize(R.dimen.thumbnail_border_width);
} else {
mBullet = null;
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);
+ mThumbnailStrokeWidth = 0;
}
mTitle = (TextView) itemView.findViewById(android.R.id.title);
@@ -106,6 +113,13 @@ final class GridDocumentHolder extends DocumentHolder {
mIconBadge = (ImageView) itemView.findViewById(R.id.icon_profile_badge);
mPreviewIcon = itemView.findViewById(R.id.preview_icon);
+ if (isUseMaterial3FlagEnabled()) {
+ int clipCornerRadius = context.getResources()
+ .getDimensionPixelSize(R.dimen.thumbnail_clip_corner_radius);
+ IconUtils.applyThumbnailClipOutline(
+ mIconThumb, mThumbnailStrokeWidth, clipCornerRadius);
+ }
+
mIconHelper = iconHelper;
if (SdkLevel.isAtLeastT() && !mConfigStore.isPrivateSpaceInDocsUIEnabled()) {
@@ -157,7 +171,7 @@ final class GridDocumentHolder extends DocumentHolder {
if (selected) {
mIconWrapper.setStrokeWidth(0);
} else if (mIconThumb.getDrawable() != null) {
- mIconWrapper.setStrokeWidth(THUMBNAIL_STROKE_WIDTH);
+ mIconWrapper.setStrokeWidth(mThumbnailStrokeWidth);
}
}
}
@@ -258,7 +272,7 @@ final class GridDocumentHolder extends DocumentHolder {
// Show stroke when thumbnail is loaded.
if (mIconWrapper != null) {
mIconWrapper.setStrokeWidth(
- thumbnailLoaded ? THUMBNAIL_STROKE_WIDTH : 0);
+ thumbnailLoaded ? mThumbnailStrokeWidth : 0);
}
});
} else {
@@ -293,8 +307,8 @@ final class GridDocumentHolder extends DocumentHolder {
}
}
- if (mBullet != null && (mDetails.getVisibility() == View.GONE
- || mDate.getText().isEmpty())) {
+ if (mBullet != null && (mDetails.getText() == null || mDetails.getText().length() == 0
+ || mDate.getText() == null || mDate.getText().length() == 0)) {
// There is no need for the bullet separating the details and date.
mBullet.setVisibility(View.GONE);
}
diff --git a/src/com/android/documentsui/dirlist/ListDocumentHolder.java b/src/com/android/documentsui/dirlist/ListDocumentHolder.java
index 0d0f79919..a0db097d0 100644
--- a/src/com/android/documentsui/dirlist/ListDocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/ListDocumentHolder.java
@@ -43,6 +43,7 @@ import androidx.annotation.RequiresApi;
import com.android.documentsui.ConfigStore;
import com.android.documentsui.DocumentsApplication;
+import com.android.documentsui.IconUtils;
import com.android.documentsui.R;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.Lookup;
@@ -78,6 +79,8 @@ final class ListDocumentHolder extends DocumentHolder {
private final ImageView mIconBadge;
private final View mIconLayout;
final View mPreviewIcon;
+ // It will be 0 when use_material flag is OFF.
+ private final int mThumbnailStrokeWidth;
private final IconHelper mIconHelper;
private final Lookup<String, String> mFileTypeLookup;
@@ -103,6 +106,17 @@ final class ListDocumentHolder extends DocumentHolder {
// Warning: mDetails view doesn't exists in layout-sw720dp-land layout
mDetails = (LinearLayout) itemView.findViewById(R.id.line2);
mPreviewIcon = itemView.findViewById(R.id.preview_icon);
+ if (isUseMaterial3FlagEnabled()) {
+ mThumbnailStrokeWidth =
+ context.getResources()
+ .getDimensionPixelSize(R.dimen.thumbnail_border_width);
+ int clipCornerRadius = context.getResources()
+ .getDimensionPixelSize(R.dimen.thumbnail_clip_corner_radius);
+ IconUtils.applyThumbnailClipOutline(
+ mIconThumb, mThumbnailStrokeWidth, clipCornerRadius);
+ } else {
+ mThumbnailStrokeWidth = 0;
+ }
mIconHelper = iconHelper;
mFileTypeLookup = fileTypeLookup;
@@ -152,7 +166,7 @@ final class ListDocumentHolder extends DocumentHolder {
if (selected) {
mIconWrapper.setStrokeWidth(0);
} else if (mIconThumb.getDrawable() != null) {
- mIconWrapper.setStrokeWidth(2);
+ mIconWrapper.setStrokeWidth(mThumbnailStrokeWidth);
}
}
}
@@ -271,7 +285,8 @@ final class ListDocumentHolder extends DocumentHolder {
thumbnailLoaded -> {
// Show stroke when thumbnail is loaded.
if (isUseMaterial3FlagEnabled() && mIconWrapper != null) {
- mIconWrapper.setStrokeWidth(thumbnailLoaded ? THUMBNAIL_STROKE_WIDTH : 0);
+ mIconWrapper.setStrokeWidth(
+ thumbnailLoaded ? mThumbnailStrokeWidth : 0);
}
});
diff --git a/src/com/android/documentsui/dirlist/SelectionMetadata.java b/src/com/android/documentsui/dirlist/SelectionMetadata.java
index 74b6061b3..0d0644502 100644
--- a/src/com/android/documentsui/dirlist/SelectionMetadata.java
+++ b/src/com/android/documentsui/dirlist/SelectionMetadata.java
@@ -18,6 +18,7 @@ package com.android.documentsui.dirlist;
import static com.android.documentsui.base.DocumentInfo.getCursorInt;
import static com.android.documentsui.base.DocumentInfo.getCursorString;
+import static com.android.documentsui.util.FlagUtils.isZipNgFlagEnabled;
import android.database.Cursor;
import android.provider.DocumentsContract.Document;
@@ -56,7 +57,13 @@ public class SelectionMetadata extends SelectionObserver<String>
private int mWritableDirectoryCount = 0;
private int mNoDeleteCount = 0;
private int mNoRenameCount = 0;
+
+ /** Number of files that are located in mounted archives. */
private int mInArchiveCount = 0;
+
+ /** Number of archives. */
+ private int mArchiveCount = 0;
+
private boolean mSupportsSettings = false;
public SelectionMetadata(Function<String, Cursor> docFinder) {
@@ -79,6 +86,9 @@ public class SelectionMetadata extends SelectionObserver<String>
mDirectoryCount += delta;
} else {
mFileCount += delta;
+ if (ArchivesProvider.isSupportedArchiveType(mimeType)) {
+ mArchiveCount += delta;
+ }
}
final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
@@ -97,9 +107,8 @@ public class SelectionMetadata extends SelectionObserver<String>
if ((docFlags & Document.FLAG_PARTIAL) != 0) {
mPartialCount += delta;
}
- mSupportsSettings = (docFlags & Document.FLAG_SUPPORTS_SETTINGS) != 0 &&
- (mFileCount + mDirectoryCount) == 1;
+ mSupportsSettings = (docFlags & Document.FLAG_SUPPORTS_SETTINGS) != 0 && size() == 1;
final String authority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
if (ArchivesProvider.AUTHORITY.equals(authority)) {
@@ -115,6 +124,8 @@ public class SelectionMetadata extends SelectionObserver<String>
mWritableDirectoryCount = 0;
mNoDeleteCount = 0;
mNoRenameCount = 0;
+ mInArchiveCount = 0;
+ mArchiveCount = 0;
}
@Override
@@ -143,6 +154,11 @@ public class SelectionMetadata extends SelectionObserver<String>
}
@Override
+ public boolean isArchive() {
+ return mDirectoryCount == 0 && mFileCount == 1 && mArchiveCount == 1;
+ }
+
+ @Override
public boolean canDelete() {
return size() > 0 && mNoDeleteCount == 0;
}
@@ -169,6 +185,7 @@ public class SelectionMetadata extends SelectionObserver<String>
@Override
public boolean canOpen() {
- return size() == 1 && mDirectoryCount == 0 && mInArchiveCount == 0 && mPartialCount == 0;
+ return mFileCount == 1 && mDirectoryCount == 0 && mPartialCount == 0 && (
+ isZipNgFlagEnabled() || mInArchiveCount == 0);
}
}
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index 50e266d38..cb8708f0b 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -210,9 +210,13 @@ public class FilesActivity extends BaseActivity implements AbstractActionHandler
updateTaskDescription(intent);
}
- // Set save container background to transparent for edge to edge nav bar.
- View saveContainer = findViewById(R.id.container_save);
- saveContainer.setBackgroundColor(Color.TRANSPARENT);
+ // When the use_material3 flag is on, the file path bar is at the bottom of the layout and
+ // hence the edge to edge nav bar is no longer required.
+ if (!isUseMaterial3FlagEnabled()) {
+ // Set save container background to transparent for edge to edge nav bar.
+ View saveContainer = findViewById(R.id.container_save);
+ saveContainer.setBackgroundColor(Color.TRANSPARENT);
+ }
presentFileErrors(icicle, intent);
}
diff --git a/src/com/android/documentsui/files/MenuManager.java b/src/com/android/documentsui/files/MenuManager.java
index 9b3564eeb..2c85dcb01 100644
--- a/src/com/android/documentsui/files/MenuManager.java
+++ b/src/com/android/documentsui/files/MenuManager.java
@@ -17,6 +17,7 @@
package com.android.documentsui.files;
import static com.android.documentsui.util.FlagUtils.isDesktopFileHandlingFlagEnabled;
+import static com.android.documentsui.util.FlagUtils.isVisualSignalsFlagEnabled;
import android.content.Context;
import android.content.res.Resources;
@@ -29,9 +30,12 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.selection.SelectionTracker;
+import com.android.documentsui.JobPanelController;
import com.android.documentsui.R;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.Features;
@@ -55,6 +59,7 @@ public final class MenuManager extends com.android.documentsui.MenuManager {
private final SelectionTracker<String> mSelectionManager;
private final Lookup<String, Uri> mUriLookup;
private final LookupApplicationName mAppNameLookup;
+ @Nullable private final JobPanelController mJobPanelController;
public MenuManager(
Features features,
@@ -74,6 +79,12 @@ public final class MenuManager extends com.android.documentsui.MenuManager {
mSelectionManager = selectionManager;
mAppNameLookup = appNameLookup;
mUriLookup = uriLookup;
+
+ if (isVisualSignalsFlagEnabled()) {
+ mJobPanelController = new JobPanelController(context);
+ } else {
+ mJobPanelController = null;
+ }
}
@Override
@@ -141,6 +152,14 @@ public final class MenuManager extends com.android.documentsui.MenuManager {
}
@Override
+ public void instantiateJobProgress(Menu menu) {
+ if (mJobPanelController == null) {
+ return;
+ }
+ mJobPanelController.setMenuItem(menu.findItem(R.id.option_menu_job_progress));
+ }
+
+ @Override
protected void updateSettings(MenuItem settings, RootInfo root) {
Menus.setEnabledAndVisible(settings, root.hasSettings());
}
@@ -212,6 +231,16 @@ public final class MenuManager extends com.android.documentsui.MenuManager {
}
@Override
+ protected void updateExtractHere(@NonNull MenuItem it, @NonNull SelectionDetails selection) {
+ Menus.setEnabledAndVisible(it, selection.isArchive());
+ }
+
+ @Override
+ protected void updateBrowse(@NonNull MenuItem it, @NonNull SelectionDetails selection) {
+ Menus.setEnabledAndVisible(it, selection.isArchive());
+ }
+
+ @Override
protected void updatePasteInto(MenuItem pasteInto, SelectionDetails selectionDetails) {
Menus.setEnabledAndVisible(pasteInto,
selectionDetails.canPasteInto() && mDirDetails.hasItemsToPaste());
diff --git a/src/com/android/documentsui/loaders/BaseFileLoader.kt b/src/com/android/documentsui/loaders/BaseFileLoader.kt
index dd76217ac..fcb1d4cb0 100644
--- a/src/com/android/documentsui/loaders/BaseFileLoader.kt
+++ b/src/com/android/documentsui/loaders/BaseFileLoader.kt
@@ -77,7 +77,7 @@ abstract class BaseFileLoader(
private var mResult: DirectoryResult? = null
override fun cancelLoadInBackground() {
- Log.d(TAG, "BasedFileLoader.cancelLoadInBackground")
+ Log.d(TAG, "${this::class.simpleName}.cancelLoadInBackground")
super.cancelLoadInBackground()
synchronized(this) {
@@ -86,7 +86,7 @@ abstract class BaseFileLoader(
}
override fun deliverResult(result: DirectoryResult?) {
- Log.d(TAG, "BasedFileLoader.deliverResult")
+ Log.d(TAG, "${this::class.simpleName}.deliverResult")
if (isReset) {
closeResult(result)
return
@@ -104,7 +104,7 @@ abstract class BaseFileLoader(
}
override fun onStartLoading() {
- Log.d(TAG, "BasedFileLoader.onStartLoading")
+ Log.d(TAG, "${this::class.simpleName}.onStartLoading")
val isCursorStale: Boolean = checkIfCursorStale(mResult)
if (mResult != null && !isCursorStale) {
deliverResult(mResult)
@@ -115,17 +115,17 @@ abstract class BaseFileLoader(
}
override fun onStopLoading() {
- Log.d(TAG, "BasedFileLoader.onStopLoading")
+ Log.d(TAG, "${this::class.simpleName}.onStopLoading")
cancelLoad()
}
override fun onCanceled(result: DirectoryResult?) {
- Log.d(TAG, "BasedFileLoader.onCanceled")
+ Log.d(TAG, "${this::class.simpleName}.onCanceled")
closeResult(result)
}
override fun onReset() {
- Log.d(TAG, "BasedFileLoader.onReset")
+ Log.d(TAG, "${this::class.simpleName}.onReset")
super.onReset()
// Ensure the loader is stopped
diff --git a/src/com/android/documentsui/loaders/FolderLoader.kt b/src/com/android/documentsui/loaders/FolderLoader.kt
index a166ca752..40c15dfe1 100644
--- a/src/com/android/documentsui/loaders/FolderLoader.kt
+++ b/src/com/android/documentsui/loaders/FolderLoader.kt
@@ -60,7 +60,7 @@ class FolderLoader(
mListedDir.authority,
mListedDir.documentId
)
- var cursor =
+ val cursor =
queryLocation(mRoot.rootId, folderChildrenUri, mOptions.otherQueryArgs, ALL_RESULTS)
?: emptyCursor()
cursor.registerContentObserver(mObserver)
diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java
index 68a797397..4f875072e 100644
--- a/src/com/android/documentsui/picker/PickActivity.java
+++ b/src/com/android/documentsui/picker/PickActivity.java
@@ -224,9 +224,11 @@ public class PickActivity extends BaseActivity implements ActionHandler.Addons {
} else if (mState.action == ACTION_OPEN_TREE ||
mState.action == ACTION_PICK_COPY_DESTINATION) {
PickFragment.show(getSupportFragmentManager());
- } else {
+ } else if (!isUseMaterial3FlagEnabled()) {
// If PickFragment or SaveFragment does not show,
// Set save container background to transparent for edge to edge nav bar.
+ // However when the use_material3 flag is on, the file path bar is at the bottom of the
+ // layout and hence the edge to edge nav bar is no longer required.
View saveContainer = findViewById(R.id.container_save);
saveContainer.setBackgroundColor(Color.TRANSPARENT);
}
diff --git a/src/com/android/documentsui/picker/PickFragment.java b/src/com/android/documentsui/picker/PickFragment.java
index e9610a510..0d20083a8 100644
--- a/src/com/android/documentsui/picker/PickFragment.java
+++ b/src/com/android/documentsui/picker/PickFragment.java
@@ -22,7 +22,9 @@ import static com.android.documentsui.services.FileOperationService.OPERATION_DE
import static com.android.documentsui.services.FileOperationService.OPERATION_EXTRACT;
import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE;
import static com.android.documentsui.services.FileOperationService.OPERATION_UNKNOWN;
+import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled;
+import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -179,13 +181,28 @@ public class PickFragment extends Fragment {
switch (mAction) {
case State.ACTION_OPEN_TREE:
mPick.setText(getString(R.string.open_tree_button));
- mPick.setWidth(Integer.MAX_VALUE);
- mCancel.setVisibility(View.GONE);
+ // When use_material3 flag is enabled, all form factors should have the pick button
+ // wrap the text content instead of taking up the full width.
+ if (!isUseMaterial3FlagEnabled()) {
+ mCancel.setVisibility(View.GONE);
+ mPick.setWidth(Integer.MAX_VALUE);
+ mPickOverlay.setVisibility(
+ mPickTarget.isBlockedFromTree() && mRestrictScopeStorage
+ ? View.VISIBLE
+ : View.GONE);
+ } else if (!getActivity()
+ .getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_PC)) {
+ // On non-desktop devices the back gesture is used to cancel the picker, so
+ // don't show the "Cancel" button on these devices and instead enable the pick
+ // overlay which enables showing a toast when the disabled button is pressed.
+ mCancel.setVisibility(View.GONE);
+ mPickOverlay.setVisibility(
+ mPickTarget.isBlockedFromTree() && mRestrictScopeStorage
+ ? View.VISIBLE
+ : View.GONE);
+ }
mPick.setEnabled(!(mPickTarget.isBlockedFromTree() && mRestrictScopeStorage));
- mPickOverlay.setVisibility(
- mPickTarget.isBlockedFromTree() && mRestrictScopeStorage
- ? View.VISIBLE
- : View.GONE);
break;
case State.ACTION_PICK_COPY_DESTINATION:
int titleId;
diff --git a/src/com/android/documentsui/picker/SaveFragment.java b/src/com/android/documentsui/picker/SaveFragment.java
index f881768b9..8316688e7 100644
--- a/src/com/android/documentsui/picker/SaveFragment.java
+++ b/src/com/android/documentsui/picker/SaveFragment.java
@@ -16,7 +16,11 @@
package com.android.documentsui.picker;
+import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled;
+
import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
@@ -42,6 +46,9 @@ import com.android.documentsui.base.BooleanConsumer;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.Shared;
+import com.google.android.material.button.MaterialButton;
+import com.google.android.material.textfield.TextInputLayout;
+
/**
* Display document title editor and save button.
*/
@@ -54,6 +61,7 @@ public class SaveFragment extends Fragment {
private DocumentInfo mReplaceTarget;
private EditText mDisplayName;
private TextView mSave;
+ private MaterialButton mCancel;
private ProgressBar mProgress;
private boolean mIgnoreNextEdit;
@@ -84,9 +92,16 @@ public class SaveFragment extends Fragment {
final View view = inflater.inflate(R.layout.fragment_save, container, false);
- final ImageView icon = (ImageView) view.findViewById(android.R.id.icon);
- icon.setImageDrawable(
- IconUtils.loadMimeIcon(context, getArguments().getString(EXTRA_MIME_TYPE)));
+ final Drawable icon =
+ IconUtils.loadMimeIcon(context, getArguments().getString(EXTRA_MIME_TYPE));
+ if (isUseMaterial3FlagEnabled()) {
+ final TextInputLayout titleWrapper =
+ (TextInputLayout) view.findViewById(R.id.title_wrapper);
+ titleWrapper.setStartIconDrawable(icon);
+ } else {
+ final ImageView iconHolder = view.findViewById(android.R.id.icon);
+ iconHolder.setImageDrawable(icon);
+ }
mDisplayName = (EditText) view.findViewById(android.R.id.title);
mDisplayName.addTextChangedListener(mDisplayNameWatcher);
@@ -122,6 +137,19 @@ public class SaveFragment extends Fragment {
mSave.setOnClickListener(mSaveListener);
mSave.setEnabled(false);
+ mCancel = (MaterialButton) view.findViewById(android.R.id.button2);
+ // For >600dp, this button is always available (via the values-600dp layout override).
+ // However on smaller layouts, the button is default GONE to save on space (the back gesture
+ // can cancel the saver) and when FEATURE_PC is set a cancel button is required due to the
+ // lack of a back gesture (mainly mouse support).
+ if (isUseMaterial3FlagEnabled()
+ && mCancel != null
+ && context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_PC)) {
+ mCancel.setOnClickListener(mCancelListener);
+ mCancel.setVisibility(View.VISIBLE);
+ mCancel.setEnabled(true);
+ }
+
mProgress = (ProgressBar) view.findViewById(android.R.id.progress);
return view;
@@ -173,6 +201,13 @@ public class SaveFragment extends Fragment {
};
+ private View.OnClickListener mCancelListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mInjector.actions.finishPicking();
+ }
+ };
+
private void performSave() {
if (mReplaceTarget != null) {
mInjector.actions.saveDocument(getChildFragmentManager(), mReplaceTarget);
diff --git a/src/com/android/documentsui/queries/SearchChipViewManager.java b/src/com/android/documentsui/queries/SearchChipViewManager.java
index f673b7408..bf3d1e865 100644
--- a/src/com/android/documentsui/queries/SearchChipViewManager.java
+++ b/src/com/android/documentsui/queries/SearchChipViewManager.java
@@ -387,7 +387,10 @@ public class SearchChipViewManager {
.getDimensionPixelSize(R.dimen.focus_ring_width);
chip.setChipStrokeWidth(focusRingWidth);
} else {
- chip.setChipStrokeWidth(1f);
+ final int strokeWidth = mChipGroup
+ .getResources()
+ .getDimensionPixelSize(R.dimen.search_chip_inactive_stroke_width);
+ chip.setChipStrokeWidth(strokeWidth);
}
}
@@ -518,7 +521,7 @@ public class SearchChipViewManager {
}
// Let the first checked chip can be shown.
- View parent = (View) mChipGroup.getParent();
+ View parent = (View) mChipGroup.getParent().getParent();
if (parent instanceof HorizontalScrollView) {
final int scrollToX = isRtl ? parent.getWidth() : 0;
((HorizontalScrollView) parent).smoothScrollTo(scrollToX, 0);
diff --git a/src/com/android/documentsui/services/FileOperationService.java b/src/com/android/documentsui/services/FileOperationService.java
index dcb2c1db4..14b87ef5b 100644
--- a/src/com/android/documentsui/services/FileOperationService.java
+++ b/src/com/android/documentsui/services/FileOperationService.java
@@ -68,6 +68,9 @@ public class FileOperationService extends Service implements Job.Listener {
public static final String EXTRA_OPERATION = "com.android.documentsui.OPERATION";
public static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL";
+ public static final String ACTION_PROGRESS = "com.android.documentsui.action.PROGRESS";
+ public static final String EXTRA_PROGRESS = "com.android.documentsui.PROGRESS";
+
@IntDef({
OPERATION_UNKNOWN,
OPERATION_COPY,
@@ -580,6 +583,7 @@ public class FileOperationService extends Service implements Job.Listener {
private final class GlobalJobMonitor implements Runnable {
private static final long PROGRESS_INTERVAL_MILLIS = 500L;
private boolean mRunning = false;
+ private long mLastId = 0;
private void start() {
if (!mRunning) {
@@ -602,9 +606,9 @@ public class FileOperationService extends Service implements Job.Listener {
}
Intent intent = new Intent();
intent.setPackage(getPackageName());
- intent.setAction("com.android.documentsui.PROGRESS");
- intent.putExtra("id", 0);
- intent.putParcelableArrayListExtra("progress", progress);
+ intent.setAction(ACTION_PROGRESS);
+ intent.putExtra("id", mLastId++);
+ intent.putParcelableArrayListExtra(EXTRA_PROGRESS, progress);
sendBroadcast(intent);
}
diff --git a/src/com/android/documentsui/services/Job.java b/src/com/android/documentsui/services/Job.java
index 0f432cc19..95ad8b335 100644
--- a/src/com/android/documentsui/services/Job.java
+++ b/src/com/android/documentsui/services/Job.java
@@ -76,11 +76,11 @@ abstract public class Job implements Runnable {
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_CREATED, STATE_STARTED, STATE_SET_UP, STATE_COMPLETED, STATE_CANCELED})
- @interface State {}
- static final int STATE_CREATED = 0;
- static final int STATE_STARTED = 1;
- static final int STATE_SET_UP = 2;
- static final int STATE_COMPLETED = 3;
+ public @interface State {}
+ public static final int STATE_CREATED = 0;
+ public static final int STATE_STARTED = 1;
+ public static final int STATE_SET_UP = 2;
+ public static final int STATE_COMPLETED = 3;
/**
* A job is in canceled state as long as {@link #cancel()} is called on it, even after it is
* completed.
diff --git a/src/com/android/documentsui/sorting/TableHeaderController.java b/src/com/android/documentsui/sorting/TableHeaderController.java
index cb72ac916..fda7b2713 100644
--- a/src/com/android/documentsui/sorting/TableHeaderController.java
+++ b/src/com/android/documentsui/sorting/TableHeaderController.java
@@ -28,10 +28,11 @@ import javax.annotation.Nullable;
/** View controller for table header that associates header cells in table header and columns. */
public final class TableHeaderController implements SortController.WidgetController {
private final HeaderCell mTitleCell;
- private final HeaderCell mSummaryCell;
- private final HeaderCell mSizeCell;
- private final HeaderCell mFileTypeCell;
- private final HeaderCell mDateCell;
+ // The 4 cells below will be null in compact/medium screen sizes when use_material3 flag is ON.
+ private final @Nullable HeaderCell mSummaryCell;
+ private final @Nullable HeaderCell mSizeCell;
+ private final @Nullable HeaderCell mFileTypeCell;
+ private final @Nullable HeaderCell mDateCell;
private final SortModel mModel;
// We assign this here porque each method reference creates a new object
// instance (which is wasteful).
@@ -66,10 +67,18 @@ public final class TableHeaderController implements SortController.WidgetControl
private void onModelUpdate(SortModel model, int updateTypeUnspecified) {
bindCell(mTitleCell, SortModel.SORT_DIMENSION_ID_TITLE);
- bindCell(mSummaryCell, SortModel.SORT_DIMENSION_ID_SUMMARY);
- bindCell(mSizeCell, SortModel.SORT_DIMENSION_ID_SIZE);
- bindCell(mFileTypeCell, SortModel.SORT_DIMENSION_ID_FILE_TYPE);
- bindCell(mDateCell, SortModel.SORT_DIMENSION_ID_DATE);
+ if (mSummaryCell != null) {
+ bindCell(mSummaryCell, SortModel.SORT_DIMENSION_ID_SUMMARY);
+ }
+ if (mSizeCell != null) {
+ bindCell(mSizeCell, SortModel.SORT_DIMENSION_ID_SIZE);
+ }
+ if (mFileTypeCell != null) {
+ bindCell(mFileTypeCell, SortModel.SORT_DIMENSION_ID_FILE_TYPE);
+ }
+ if (mDateCell != null) {
+ bindCell(mDateCell, SortModel.SORT_DIMENSION_ID_DATE);
+ }
}
@Override
diff --git a/src/com/android/documentsui/util/ColorUtils.kt b/src/com/android/documentsui/util/ColorUtils.kt
new file mode 100644
index 000000000..ee67b7832
--- /dev/null
+++ b/src/com/android/documentsui/util/ColorUtils.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.util
+
+import android.content.Context
+import android.util.TypedValue
+import androidx.annotation.AttrRes
+
+class ColorUtils {
+ companion object {
+ /**
+ * Resolve a color attribute from the Material3 theme, example usage.
+ * resolveMaterialColorAttribute(context, com.google.android.material.R.attr.XXX).
+ */
+ @JvmStatic
+ fun resolveMaterialColorAttribute(context: Context, @AttrRes colorAttrId: Int): Int {
+ val typedValue = TypedValue()
+ context.theme.resolveAttribute(colorAttrId, typedValue, true)
+ return typedValue.data
+ }
+ }
+}
diff --git a/src/com/android/documentsui/util/FlagUtils.kt b/src/com/android/documentsui/util/FlagUtils.kt
index eee51be89..a041dde44 100644
--- a/src/com/android/documentsui/util/FlagUtils.kt
+++ b/src/com/android/documentsui/util/FlagUtils.kt
@@ -31,12 +31,12 @@ class FlagUtils {
@JvmStatic
fun isZipNgFlagEnabled(): Boolean {
- return Flags.zipNgRo()
+ return Flags.zipNgRo() && Flags.useMaterial3()
}
@JvmStatic
- fun isUseSearchV2RwFlagEnabled(): Boolean {
- return Flags.useSearchV2Rw()
+ fun isUseSearchV2FlagEnabled(): Boolean {
+ return Flags.useSearchV2ReadOnly()
}
@JvmStatic