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,
+    )
+}