diff options
Diffstat (limited to 'src')
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 |