[Predictive Back] Refactor setClipChildrenOnViewTree/restoreClipChildrenOnViewTree to support clipToPadding

Bug: 325930715
Test: Added unit test
Flag: aconfig com.android.launcher3.enable_predictive_back_gesture TEAMFOOD
Change-Id: I509546ac4ee1fa851cf0648d365a5348362267cc
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index d3019c5..d44438f 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -68,8 +68,6 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.view.ViewParent;
 import android.view.animation.Interpolator;
 
 import androidx.annotation.ChecksSdkIntAtLeast;
@@ -775,136 +773,6 @@
     }
 
     /**
-     * Recursively call {@link ViewGroup#setClipChildren(boolean)} from {@link View} to ts parent
-     * (direct or indirect) inclusive. This method will also save the old clipChildren value on each
-     * view with {@link View#setTag(int, Object)}, which can be restored in
-     * {@link #restoreClipChildrenOnViewTree(View, ViewParent)}.
-     *
-     * Note that if parent is null or not a parent of the view, this method will be applied all the
-     * way to root view.
-     *
-     * @param v child view
-     * @param parent direct or indirect parent of child view
-     * @param clipChildren whether we should clip children
-     */
-    public static void setClipChildrenOnViewTree(
-            @Nullable View v,
-            @Nullable ViewParent parent,
-            boolean clipChildren) {
-        if (v == null) {
-            return;
-        }
-
-        if (v instanceof ViewGroup) {
-            ViewGroup viewGroup = (ViewGroup) v;
-            boolean oldClipChildren = viewGroup.getClipChildren();
-            if (oldClipChildren != clipChildren) {
-                v.setTag(R.id.saved_clip_children_tag_id, oldClipChildren);
-                viewGroup.setClipChildren(clipChildren);
-            }
-        }
-
-        if (v == parent) {
-            return;
-        }
-
-        if (v.getParent() instanceof View) {
-            setClipChildrenOnViewTree((View) v.getParent(), parent, clipChildren);
-        }
-    }
-
-    /**
-     * Recursively call {@link ViewGroup#setClipChildren(boolean)} to restore clip children value
-     * set in {@link #setClipChildrenOnViewTree(View, ViewParent, boolean)} on view to its parent
-     * (direct or indirect) inclusive.
-     *
-     * Note that if parent is null or not a parent of the view, this method will be applied all the
-     * way to root view.
-     *
-     * @param v child view
-     * @param parent direct or indirect parent of child view
-     */
-    public static void restoreClipChildrenOnViewTree(
-            @Nullable View v, @Nullable ViewParent parent) {
-        if (v == null) {
-            return;
-        }
-        if (v instanceof ViewGroup) {
-            ViewGroup viewGroup = (ViewGroup) v;
-            Object viewTag = viewGroup.getTag(R.id.saved_clip_children_tag_id);
-            if (viewTag instanceof Boolean) {
-                viewGroup.setClipChildren((boolean) viewTag);
-                viewGroup.setTag(R.id.saved_clip_children_tag_id, null);
-            }
-        }
-
-        if (v == parent) {
-            return;
-        }
-
-        if (v.getParent() instanceof View) {
-            restoreClipChildrenOnViewTree((View) v.getParent(), parent);
-        }
-    }
-
-    /**
-     * Similar to {@link #setClipChildrenOnViewTree(View, ViewParent, boolean)} but is calling
-     * {@link ViewGroup#setClipToPadding}.
-     */
-    public static void setClipToPaddingOnViewTree(
-            @Nullable View v,
-            @Nullable ViewParent parent,
-            boolean clipToPadding) {
-        if (v == null) {
-            return;
-        }
-
-        if (v instanceof ViewGroup) {
-            ViewGroup viewGroup = (ViewGroup) v;
-            boolean oldClipToPadding = viewGroup.getClipToPadding();
-            if (oldClipToPadding != clipToPadding) {
-                v.setTag(R.id.saved_clip_to_padding_tag_id, oldClipToPadding);
-                viewGroup.setClipToPadding(clipToPadding);
-            }
-        }
-
-        if (v == parent) {
-            return;
-        }
-
-        if (v.getParent() instanceof View) {
-            setClipToPaddingOnViewTree((View) v.getParent(), parent, clipToPadding);
-        }
-    }
-
-    /**
-     * Similar to {@link #restoreClipChildrenOnViewTree(View, ViewParent)} but is calling
-     * {@link ViewGroup#setClipToPadding}.
-     */
-    public static void restoreClipToPaddingOnViewTree(
-            @Nullable View v, @Nullable ViewParent parent) {
-        if (v == null) {
-            return;
-        }
-        if (v instanceof ViewGroup) {
-            ViewGroup viewGroup = (ViewGroup) v;
-            Object viewTag = viewGroup.getTag(R.id.saved_clip_to_padding_tag_id);
-            if (viewTag instanceof Boolean) {
-                viewGroup.setClipToPadding((boolean) viewTag);
-                viewGroup.setTag(R.id.saved_clip_to_padding_tag_id, null);
-            }
-        }
-
-        if (v == parent) {
-            return;
-        }
-
-        if (v.getParent() instanceof View) {
-            restoreClipToPaddingOnViewTree((View) v.getParent(), parent);
-        }
-    }
-
-    /**
      * Translates the {@code targetView} so that it overlaps with {@code exclusionBounds} as little
      * as possible, while remaining within {@code inclusionBounds}.
      * <p>
diff --git a/src/com/android/launcher3/UtilitiesKt.kt b/src/com/android/launcher3/UtilitiesKt.kt
new file mode 100644
index 0000000..a207d57
--- /dev/null
+++ b/src/com/android/launcher3/UtilitiesKt.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3
+
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewParent
+
+object UtilitiesKt {
+
+    /**
+     * Modify [ViewGroup]'s attribute with type [T]. The overridden attribute is saved by calling
+     * [View.setTag] and can be later restored by [View.getTag].
+     *
+     * @param <T> type of [ViewGroup] attribute. For example, [T] is [Boolean] if modifying
+     *   [ViewGroup.setClipChildren]
+     */
+    abstract class ViewGroupAttrModifier<T>(
+        private val targetAttrValue: T,
+        private val tagKey: Int
+    ) {
+        /**
+         * If [targetAttrValue] is different from existing view attribute returned from
+         * [getAttribute], this method will save existing attribute by calling [ViewGroup.setTag].
+         * Then call [setAttribute] to set attribute with [targetAttrValue].
+         */
+        fun saveAndChangeAttribute(viewGroup: ViewGroup) {
+            val oldAttrValue = getAttribute(viewGroup)
+            if (oldAttrValue !== targetAttrValue) {
+                viewGroup.setTag(tagKey, oldAttrValue)
+                setAttribute(viewGroup, targetAttrValue)
+            }
+        }
+
+        /** Restore saved attribute in [saveAndChangeAttribute] by calling [ViewGroup.getTag]. */
+        @Suppress("UNCHECKED_CAST")
+        fun restoreAttribute(viewGroup: ViewGroup) {
+            val oldAttrValue: T = viewGroup.getTag(tagKey) as T ?: return
+            setAttribute(viewGroup, oldAttrValue)
+            viewGroup.setTag(tagKey, null)
+        }
+
+        /** Subclass will override this method to decide how to get [ViewGroup] attribute. */
+        abstract fun getAttribute(viewGroup: ViewGroup): T
+
+        /** Subclass will override this method to decide how to set [ViewGroup] attribute. */
+        abstract fun setAttribute(viewGroup: ViewGroup, attr: T)
+    }
+
+    /** [ViewGroupAttrModifier] to call [ViewGroup.setClipChildren] to false. */
+    @JvmField
+    val CLIP_CHILDREN_FALSE_MODIFIER: ViewGroupAttrModifier<Boolean> =
+        object : ViewGroupAttrModifier<Boolean>(false, R.id.saved_clip_children_tag_id) {
+            override fun getAttribute(viewGroup: ViewGroup): Boolean {
+                return viewGroup.clipChildren
+            }
+
+            override fun setAttribute(viewGroup: ViewGroup, clipChildren: Boolean) {
+                viewGroup.clipChildren = clipChildren
+            }
+        }
+
+    /** [ViewGroupAttrModifier] to call [ViewGroup.setClipToPadding] to false. */
+    @JvmField
+    val CLIP_TO_PADDING_FALSE_MODIFIER: ViewGroupAttrModifier<Boolean> =
+        object : ViewGroupAttrModifier<Boolean>(false, R.id.saved_clip_to_padding_tag_id) {
+            override fun getAttribute(viewGroup: ViewGroup): Boolean {
+                return viewGroup.clipToPadding
+            }
+
+            override fun setAttribute(viewGroup: ViewGroup, clipToPadding: Boolean) {
+                viewGroup.clipToPadding = clipToPadding
+            }
+        }
+
+    /**
+     * Recursively call [ViewGroupAttrModifier.saveAndChangeAttribute] from [View] to its parent
+     * (direct or indirect) inclusive.
+     *
+     * [ViewGroupAttrModifier.saveAndChangeAttribute] will save the existing attribute value on each
+     * view with [View.setTag], which can be restored in [restoreAttributesOnViewTree].
+     *
+     * Note that if parent is null or not a parent of the view, this method will be applied all the
+     * way to root view.
+     *
+     * @param v child view
+     * @param parent direct or indirect parent of child view
+     * @param modifiers list of [ViewGroupAttrModifier] to modify view attribute
+     */
+    @JvmStatic
+    fun modifyAttributesOnViewTree(
+        v: View?,
+        parent: ViewParent?,
+        vararg modifiers: ViewGroupAttrModifier<*>
+    ) {
+        if (v == null) {
+            return
+        }
+        if (v is ViewGroup) {
+            for (modifier in modifiers) {
+                modifier.saveAndChangeAttribute(v)
+            }
+        }
+        if (v === parent) {
+            return
+        }
+        if (v.parent is View) {
+            modifyAttributesOnViewTree(v.parent as View, parent, *modifiers)
+        }
+    }
+
+    /**
+     * Recursively call [ViewGroupAttrModifier.restoreAttribute]} to restore view attributes
+     * previously saved in [ViewGroupAttrModifier.saveAndChangeAttribute] on view to its parent
+     * (direct or indirect) inclusive.
+     *
+     * Note that if parent is null or not a parent of the view, this method will be applied all the
+     * way to root view.
+     *
+     * @param v child view
+     * @param parent direct or indirect parent of child view
+     * @param modifiers list of [ViewGroupAttrModifier] to restore view attributes
+     */
+    @JvmStatic
+    fun restoreAttributesOnViewTree(
+        v: View?,
+        parent: ViewParent?,
+        vararg modifiers: ViewGroupAttrModifier<*>
+    ) {
+        if (v == null) {
+            return
+        }
+        if (v is ViewGroup) {
+            for (modifier in modifiers) {
+                modifier.restoreAttribute(v)
+            }
+        }
+        if (v === parent) {
+            return
+        }
+        if (v.parent is View) {
+            restoreAttributesOnViewTree(v.parent as View, parent, *modifiers)
+        }
+    }
+}
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index aeef5b8..3678109 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -24,8 +24,9 @@
 import static com.android.launcher3.LauncherState.ALL_APPS_CONTENT;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.Utilities.restoreClipChildrenOnViewTree;
-import static com.android.launcher3.Utilities.setClipChildrenOnViewTree;
+import static com.android.launcher3.UtilitiesKt.CLIP_CHILDREN_FALSE_MODIFIER;
+import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree;
+import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree;
 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_BOTTOM_SHEET_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
@@ -305,9 +306,11 @@
         if (hasScaleEffect != mHasScaleEffect) {
             mHasScaleEffect = hasScaleEffect;
             if (mHasScaleEffect) {
-                setClipChildrenOnViewTree(rv, mLauncher.getAppsView(), false);
+                modifyAttributesOnViewTree(rv, mLauncher.getAppsView(),
+                        CLIP_CHILDREN_FALSE_MODIFIER);
             } else {
-                restoreClipChildrenOnViewTree(rv, mLauncher.getAppsView());
+                restoreAttributesOnViewTree(rv, mLauncher.getAppsView(),
+                        CLIP_CHILDREN_FALSE_MODIFIER);
             }
         }
     }
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index 1e17252..a9487b7 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -17,10 +17,10 @@
 
 import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
 import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
-import static com.android.launcher3.Utilities.restoreClipChildrenOnViewTree;
-import static com.android.launcher3.Utilities.restoreClipToPaddingOnViewTree;
-import static com.android.launcher3.Utilities.setClipChildrenOnViewTree;
-import static com.android.launcher3.Utilities.setClipToPaddingOnViewTree;
+import static com.android.launcher3.UtilitiesKt.CLIP_CHILDREN_FALSE_MODIFIER;
+import static com.android.launcher3.UtilitiesKt.CLIP_TO_PADDING_FALSE_MODIFIER;
+import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree;
+import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree;
 
 import android.content.Context;
 import android.graphics.Outline;
@@ -156,13 +156,15 @@
         }
         mOldIsBackSwipeProgressing = isBackSwipeProgressing;
         if (isBackSwipeProgressing) {
-            setClipChildrenOnViewTree(mPrimaryWidgetListView, (ViewParent) mContent, false);
-            setClipChildrenOnViewTree(mRightPaneScrollView, (ViewParent) mContent, false);
-            setClipToPaddingOnViewTree(mRightPaneScrollView, (ViewParent) mContent, false);
+            modifyAttributesOnViewTree(mPrimaryWidgetListView, (ViewParent) mContent,
+                    CLIP_CHILDREN_FALSE_MODIFIER);
+            modifyAttributesOnViewTree(mRightPaneScrollView,  (ViewParent) mContent,
+                    CLIP_CHILDREN_FALSE_MODIFIER, CLIP_TO_PADDING_FALSE_MODIFIER);
         } else {
-            restoreClipChildrenOnViewTree(mPrimaryWidgetListView, mContent);
-            restoreClipChildrenOnViewTree(mRightPaneScrollView, mContent);
-            restoreClipToPaddingOnViewTree(mRightPaneScrollView, mContent);
+            restoreAttributesOnViewTree(mPrimaryWidgetListView, mContent,
+                    CLIP_CHILDREN_FALSE_MODIFIER);
+            restoreAttributesOnViewTree(mRightPaneScrollView, mContent,
+                    CLIP_CHILDREN_FALSE_MODIFIER, CLIP_TO_PADDING_FALSE_MODIFIER);
         }
     }