Create an action row view
Create a new widget, ActionRow, that encapsulates action row UI under an
generalized interface.
A preparation step for having an alternative action row UI implementation
protected by a feature flag.
Bug: 262278109
Test: manual test for all preview types and actions in both cases, when
a preview is a part of the recycle view and when it's detached, for UI
and functionality (a11y included)
Test: atest IntentResolverUnitTests
Change-Id: I3e78a85386f6ea49feebeef8b15e2b6d2d6e9234
diff --git a/java/res/layout/chooser_action_row.xml b/java/res/layout/chooser_action_row.xml
index ea75611..fd47155 100644
--- a/java/res/layout/chooser_action_row.xml
+++ b/java/res/layout/chooser_action_row.xml
@@ -14,13 +14,10 @@
~ limitations under the License
-->
-<LinearLayout
+<com.android.intentresolver.widget.ActionRow
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/chooser_edge_margin_normal"
android:paddingRight="@dimen/chooser_edge_margin_normal"
- android:gravity="center"
- >
-
-</LinearLayout>
+ android:gravity="center" />
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 6d5304d..6cf1aef 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -77,7 +77,6 @@
import android.util.Size;
import android.util.Slog;
import android.util.SparseArray;
-import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
@@ -86,7 +85,6 @@
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
-import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.MainThread;
@@ -97,7 +95,6 @@
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState;
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
-import com.android.intentresolver.ResolverListAdapter.ViewHolder;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
@@ -108,6 +105,7 @@
import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
import com.android.intentresolver.shortcuts.AppPredictorFactory;
import com.android.intentresolver.shortcuts.ShortcutLoader;
+import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.ResolverDrawerLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
@@ -629,7 +627,7 @@
updateProfileViewButton();
}
- private void onCopyButtonClicked(View v) {
+ private void onCopyButtonClicked() {
Intent targetIntent = getTargetIntent();
if (targetIntent == null) {
finish();
@@ -752,21 +750,23 @@
int previewType = ChooserContentPreviewUi.findPreferredContentPreview(
targetIntent, getContentResolver(), this::isImageType);
- ChooserContentPreviewUi.ActionButtonFactory buttonFactory =
- new ChooserContentPreviewUi.ActionButtonFactory() {
+ ChooserContentPreviewUi.ActionFactory actionFactory =
+ new ChooserContentPreviewUi.ActionFactory() {
@Override
- public Button createCopyButton() {
- return ChooserActivity.this.createCopyButton();
+ public ActionRow.Action createCopyButton() {
+ return ChooserActivity.this.createCopyAction();
}
+ @Nullable
@Override
- public Button createEditButton() {
- return ChooserActivity.this.createEditButton(targetIntent);
+ public ActionRow.Action createEditButton() {
+ return ChooserActivity.this.createEditAction(targetIntent);
}
+ @Nullable
@Override
- public Button createNearbyButton() {
- return ChooserActivity.this.createNearbyButton(targetIntent);
+ public ActionRow.Action createNearbyButton() {
+ return ChooserActivity.this.createNearbyAction(targetIntent);
}
};
@@ -775,7 +775,7 @@
targetIntent,
getResources(),
getLayoutInflater(),
- buttonFactory,
+ actionFactory,
parent,
previewCoordinator,
getContentResolver(),
@@ -902,54 +902,46 @@
return dri;
}
- private Button createActionButton(Drawable icon, CharSequence title, View.OnClickListener r) {
- Button b = (Button) LayoutInflater.from(this).inflate(R.layout.chooser_action_button, null);
- if (icon != null) {
- final int size = getResources()
- .getDimensionPixelSize(R.dimen.chooser_action_button_icon_size);
- icon.setBounds(0, 0, size, size);
- b.setCompoundDrawablesRelative(icon, null, null, null);
- }
- b.setText(title);
- b.setOnClickListener(r);
- return b;
- }
-
- private Button createCopyButton() {
- final Button b = createActionButton(
+ private ActionRow.Action createCopyAction() {
+ return new ActionRow.Action(
+ com.android.internal.R.id.chooser_copy_button,
+ getString(com.android.internal.R.string.copy),
getDrawable(com.android.internal.R.drawable.ic_menu_copy_material),
- getString(com.android.internal.R.string.copy), this::onCopyButtonClicked);
- b.setId(com.android.internal.R.id.chooser_copy_button);
- return b;
+ this::onCopyButtonClicked);
}
- private @Nullable Button createNearbyButton(Intent originalIntent) {
+ @Nullable
+ private ActionRow.Action createNearbyAction(Intent originalIntent) {
final TargetInfo ti = getNearbySharingTarget(originalIntent);
- if (ti == null) return null;
+ if (ti == null) {
+ return null;
+ }
- final Button b = createActionButton(
- ti.getDisplayIconHolder().getDisplayIcon(),
+ return new ActionRow.Action(
+ com.android.internal.R.id.chooser_nearby_button,
ti.getDisplayLabel(),
- (View unused) -> {
+ ti.getDisplayIconHolder().getDisplayIcon(),
+ () -> {
getChooserActivityLogger().logActionSelected(
ChooserActivityLogger.SELECTION_TYPE_NEARBY);
// Action bar is user-independent, always start as primary
safelyStartActivityAsUser(ti, getPersonalProfileUserHandle());
finish();
- }
- );
- b.setId(com.android.internal.R.id.chooser_nearby_button);
- return b;
+ });
}
- private @Nullable Button createEditButton(Intent originalIntent) {
+ @Nullable
+ private ActionRow.Action createEditAction(Intent originalIntent) {
final TargetInfo ti = getEditSharingTarget(originalIntent);
- if (ti == null) return null;
+ if (ti == null) {
+ return null;
+ }
- final Button b = createActionButton(
- ti.getDisplayIconHolder().getDisplayIcon(),
+ return new ActionRow.Action(
+ com.android.internal.R.id.chooser_edit_button,
ti.getDisplayLabel(),
- (View unused) -> {
+ ti.getDisplayIconHolder().getDisplayIcon(),
+ () -> {
// Log share completion via edit
getChooserActivityLogger().logActionSelected(
ChooserActivityLogger.SELECTION_TYPE_EDIT);
@@ -967,8 +959,6 @@
}
}
);
- b.setId(com.android.internal.R.id.chooser_edit_button);
- return b;
}
@Nullable
@@ -977,17 +967,6 @@
return firstImage != null && firstImage.isVisibleToUser() ? firstImage : null;
}
- private void addActionButton(ViewGroup parent, Button b) {
- if (b == null) return;
- final ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(
- LayoutParams.WRAP_CONTENT,
- LayoutParams.WRAP_CONTENT
- );
- final int gap = getResources().getDimensionPixelSize(R.dimen.resolver_icon_margin) / 2;
- lp.setMarginsRelative(gap, 0, gap, 0);
- parent.addView(b, lp);
- }
-
/**
* Wrapping the ContentResolver call to expose for easier mocking,
* and to avoid mocking Android core classes.
diff --git a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
index 22ff55d..f9f4ee9 100644
--- a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
@@ -34,11 +34,12 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewGroup.LayoutParams;
-import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.RoundedRectImageView;
import com.android.internal.annotations.VisibleForTesting;
@@ -88,15 +89,17 @@
* they're determined to be appropriate for the particular preview we display.
* TODO: clarify why action buttons are part of preview logic.
*/
- public interface ActionButtonFactory {
- /** Create a button that copies the share content to the clipboard. */
- Button createCopyButton();
+ public interface ActionFactory {
+ /** Create an action that copies the share content to the clipboard. */
+ ActionRow.Action createCopyButton();
- /** Create a button that opens the share content in a system-default editor. */
- Button createEditButton();
+ /** Create an action that opens the share content in a system-default editor. */
+ @Nullable
+ ActionRow.Action createEditButton();
- /** Create a "Share to Nearby" button. */
- Button createNearbyButton();
+ /** Create an "Share to Nearby" action. */
+ @Nullable
+ ActionRow.Action createNearbyButton();
}
/**
@@ -173,7 +176,7 @@
Intent targetIntent,
Resources resources,
LayoutInflater layoutInflater,
- ActionButtonFactory buttonFactory,
+ ActionFactory actionFactory,
ViewGroup parent,
ContentPreviewCoordinator previewCoord,
ContentResolver contentResolver,
@@ -184,18 +187,16 @@
case CONTENT_PREVIEW_TEXT:
layout = displayTextContentPreview(
targetIntent,
- resources,
layoutInflater,
- buttonFactory,
+ createTextPreviewActions(actionFactory),
parent,
previewCoord);
break;
case CONTENT_PREVIEW_IMAGE:
layout = displayImageContentPreview(
targetIntent,
- resources,
layoutInflater,
- buttonFactory,
+ createImagePreviewActions(actionFactory),
parent,
previewCoord,
contentResolver,
@@ -206,7 +207,7 @@
targetIntent,
resources,
layoutInflater,
- buttonFactory,
+ createFilePreviewActions(actionFactory),
parent,
previewCoord,
contentResolver);
@@ -235,20 +236,18 @@
private static ViewGroup displayTextContentPreview(
Intent targetIntent,
- Resources resources,
LayoutInflater layoutInflater,
- ActionButtonFactory buttonFactory,
+ List<ActionRow.Action> actions,
ViewGroup parent,
ContentPreviewCoordinator previewCoord) {
ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_text, parent, false);
- final ViewGroup actionRow =
- (ViewGroup) contentPreviewLayout.findViewById(
- com.android.internal.R.id.chooser_action_row);
- final int iconMargin = resources.getDimensionPixelSize(R.dimen.resolver_icon_margin);
- addActionButton(actionRow, buttonFactory.createCopyButton(), iconMargin);
- addActionButton(actionRow, buttonFactory.createNearbyButton(), iconMargin);
+ final ActionRow actionRow =
+ contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
+ if (actionRow != null) {
+ actionRow.setActions(actions);
+ }
CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
if (sharingText == null) {
@@ -296,11 +295,20 @@
return contentPreviewLayout;
}
+ private static List<ActionRow.Action> createTextPreviewActions(ActionFactory actionFactory) {
+ ArrayList<ActionRow.Action> actions = new ArrayList<>(2);
+ actions.add(actionFactory.createCopyButton());
+ ActionRow.Action nearbyAction = actionFactory.createNearbyButton();
+ if (nearbyAction != null) {
+ actions.add(nearbyAction);
+ }
+ return actions;
+ }
+
private static ViewGroup displayImageContentPreview(
Intent targetIntent,
- Resources resources,
LayoutInflater layoutInflater,
- ActionButtonFactory buttonFactory,
+ List<ActionRow.Action> actions,
ViewGroup parent,
ContentPreviewCoordinator previewCoord,
ContentResolver contentResolver,
@@ -310,13 +318,11 @@
ViewGroup imagePreview = contentPreviewLayout.findViewById(
com.android.internal.R.id.content_preview_image_area);
- final ViewGroup actionRow =
- (ViewGroup) contentPreviewLayout.findViewById(
- com.android.internal.R.id.chooser_action_row);
- final int iconMargin = resources.getDimensionPixelSize(R.dimen.resolver_icon_margin);
- //TODO: addActionButton(actionRow, buttonFactory.createCopyButton(), iconMargin);
- addActionButton(actionRow, buttonFactory.createNearbyButton(), iconMargin);
- addActionButton(actionRow, buttonFactory.createEditButton(), iconMargin);
+ final ActionRow actionRow =
+ contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
+ if (actionRow != null) {
+ actionRow.setActions(actions);
+ }
String action = targetIntent.getAction();
if (Intent.ACTION_SEND.equals(action)) {
@@ -375,24 +381,37 @@
return contentPreviewLayout;
}
+ private static List<ActionRow.Action> createImagePreviewActions(
+ ActionFactory buttonFactory) {
+ ArrayList<ActionRow.Action> actions = new ArrayList<>(2);
+ //TODO: add copy action;
+ ActionRow.Action action = buttonFactory.createNearbyButton();
+ if (action != null) {
+ actions.add(action);
+ }
+ action = buttonFactory.createEditButton();
+ if (action != null) {
+ actions.add(action);
+ }
+ return actions;
+ }
+
private static ViewGroup displayFileContentPreview(
Intent targetIntent,
Resources resources,
LayoutInflater layoutInflater,
- ActionButtonFactory buttonFactory,
+ List<ActionRow.Action> actions,
ViewGroup parent,
ContentPreviewCoordinator previewCoord,
ContentResolver contentResolver) {
ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_file, parent, false);
- final ViewGroup actionRow =
- (ViewGroup) contentPreviewLayout.findViewById(
- com.android.internal.R.id.chooser_action_row);
- final int iconMargin = resources.getDimensionPixelSize(R.dimen.resolver_icon_margin);
- //TODO(b/120417119):
- // addActionButton(actionRow, buttonFactory.createCopyButton(), iconMargin);
- addActionButton(actionRow, buttonFactory.createNearbyButton(), iconMargin);
+ final ActionRow actionRow =
+ contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
+ if (actionRow != null) {
+ actionRow.setActions(actions);
+ }
String action = targetIntent.getAction();
if (Intent.ACTION_SEND.equals(action)) {
@@ -438,6 +457,17 @@
return contentPreviewLayout;
}
+ private static List<ActionRow.Action> createFilePreviewActions(ActionFactory actionFactory) {
+ List<ActionRow.Action> actions = new ArrayList<>(1);
+ //TODO(b/120417119):
+ // add action buttonFactory.createCopyButton()
+ ActionRow.Action action = actionFactory.createNearbyButton();
+ if (action != null) {
+ actions.add(action);
+ }
+ return actions;
+ }
+
private static void logContentPreviewWarning(Uri uri) {
// The ContentResolver already logs the exception. Log something more informative.
Log.w(TAG, "Could not load (" + uri.toString() + ") thumbnail/name for preview. If "
@@ -475,19 +505,6 @@
}
}
- private static void addActionButton(ViewGroup parent, Button b, int iconMargin) {
- if (b == null) {
- return;
- }
- final ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(
- LayoutParams.WRAP_CONTENT,
- LayoutParams.WRAP_CONTENT
- );
- final int gap = iconMargin / 2;
- lp.setMarginsRelative(gap, 0, gap, 0);
- parent.addView(b, lp);
- }
-
private static FileInfo extractFileInfo(Uri uri, ContentResolver resolver) {
String fileName = null;
boolean hasThumbnail = false;
diff --git a/java/src/com/android/intentresolver/widget/ActionRow.kt b/java/src/com/android/intentresolver/widget/ActionRow.kt
new file mode 100644
index 0000000..1be48f3
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ActionRow.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 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.intentresolver.widget
+
+import android.annotation.LayoutRes
+import android.content.Context
+import android.content.res.Resources.ID_NULL
+import android.graphics.drawable.Drawable
+import android.os.Parcelable
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.Button
+import android.widget.LinearLayout
+import com.android.intentresolver.R
+
+// TODO: extract an interface out of the class, use it in layout hierarchy an have a layout inflater
+// to instantiate the right view based on a flag value.
+class ActionRow : LinearLayout {
+ constructor(context: Context) : this(context, null)
+ constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
+ constructor(
+ context: Context, attrs: AttributeSet?, defStyleAttr: Int
+ ) : this(context, attrs, defStyleAttr, 0)
+
+ constructor(
+ context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int
+ ) : super(context, attrs, defStyleAttr, defStyleRes) {
+ orientation = HORIZONTAL
+ }
+
+ @LayoutRes
+ private val itemLayout = R.layout.chooser_action_button
+ private val itemMargin =
+ context.resources.getDimensionPixelSize(R.dimen.resolver_icon_margin) / 2
+ private var actions: List<Action> = emptyList()
+
+ override fun onRestoreInstanceState(state: Parcelable?) {
+ super.onRestoreInstanceState(state)
+ setActions(actions)
+ }
+
+ fun setActions(actions: List<Action>) {
+ removeAllViews()
+ this.actions = ArrayList(actions)
+ for (action in actions) {
+ addAction(action)
+ }
+ }
+
+ private fun addAction(action: Action) {
+ val b = LayoutInflater.from(context).inflate(itemLayout, null) as Button
+ if (action.icon != null) {
+ val size = resources
+ .getDimensionPixelSize(R.dimen.chooser_action_button_icon_size)
+ action.icon.setBounds(0, 0, size, size)
+ b.setCompoundDrawablesRelative(action.icon, null, null, null)
+ }
+ b.text = action.label ?: ""
+ b.setOnClickListener {
+ action.onClicked.run()
+ }
+ b.id = action.id
+ addView(b)
+ }
+
+ override fun generateDefaultLayoutParams(): LayoutParams =
+ super.generateDefaultLayoutParams().apply {
+ setMarginsRelative(itemMargin, 0, itemMargin, 0)
+ }
+
+ class Action @JvmOverloads constructor(
+ // TODO: apparently, IDs set to this field are used in unit tests only; evaluate whether we
+ // get rid of them
+ val id: Int = ID_NULL,
+ val label: CharSequence?,
+ val icon: Drawable?,
+ val onClicked: Runnable,
+ )
+}