Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3017dee0 authored by Juan Sebastian Martinez's avatar Juan Sebastian Martinez Committed by Android (Google) Code Review
Browse files

Merge "Extending the number of Roundable targets during magnetic pulls." into main

parents 1b1e1114 e6c0ccbf
Loading
Loading
Loading
Loading
+101 −51
Original line number Diff line number Diff line
@@ -94,9 +94,14 @@ class NotificationTargetsHelperTest : SysuiTestCase() {
    }

    @Test
    fun findMagneticTargets_forMiddleChild_createsAllTargets() {
        val childrenNumber = 5
    fun findMagneticRoundableTargets_forMiddleChild_createsAllTargets() {
        val childrenNumber = 7
        val children = kosmos.createRowGroup(childrenNumber).childrenContainer
        val expectedTargets =
            children.attachedChildren.mapIndexed { i, child ->
                val useMagneticListener = i > 0 && i < childrenNumber - 1
                child.toMagneticRoundableTarget(useMagneticListener)
            }

        // WHEN the swiped view is the one at the middle of the container
        val swiped = children.attachedChildren[childrenNumber / 2]
@@ -104,122 +109,167 @@ class NotificationTargetsHelperTest : SysuiTestCase() {
        // THEN all the views that surround it become targets with the swiped view at the middle
        val actual =
            notificationTargetsHelper()
                .findMagneticTargets(
                .findMagneticRoundableTargets(
                    viewSwiped = swiped,
                    stackScrollLayout = stackScrollLayout,
                    sectionsManager,
                    totalMagneticTargets = 5,
                    numTargets = childrenNumber - 2, // Account for the roundable boundaries
                )
        assertMagneticTargetsForChildren(actual, children.attachedChildren)
        assertMagneticRoundableTargetsForChildren(actual, expectedTargets)
    }

    @Test
    fun findMagneticTargets_forTopChild_createsEligibleTargets() {
        val childrenNumber = 5
    fun findMagneticRoundableTargets_forTopChild_createsEligibleTargets() {
        val childrenNumber = 7
        val children = kosmos.createRowGroup(childrenNumber).childrenContainer

        // WHEN the swiped view is the first one in the container
        val swiped = children.attachedChildren[0]

        // THEN the neighboring views become targets, with the swiped view at the middle and nulls
        // to the left
        // THEN the neighboring views become targets, with the swiped view at the middle and empty
        // targets to the left, except for the first left-most neighbor, which should be the
        // notification header wrapper as a roundable.
        val actual =
            notificationTargetsHelper()
                .findMagneticTargets(
                .findMagneticRoundableTargets(
                    viewSwiped = swiped,
                    stackScrollLayout = stackScrollLayout,
                    sectionsManager,
                    totalMagneticTargets = 5,
                    numTargets = childrenNumber - 2, // Account for the roundable boundaries
                )
        val expectedTargets =
            listOf(
                MagneticRoundableTarget.Empty,
                MagneticRoundableTarget.Empty,
                MagneticRoundableTarget(children.notificationHeaderWrapper, null),
                swiped.toMagneticRoundableTarget(),
                children.attachedChildren[1].toMagneticRoundableTarget(),
                children.attachedChildren[2].toMagneticRoundableTarget(),
                children.attachedChildren[3].toMagneticRoundableTarget(useMagneticListener = false),
            )
        val expectedRows =
            listOf(null, null, swiped, children.attachedChildren[1], children.attachedChildren[2])
        assertMagneticTargetsForChildren(actual, expectedRows)
        assertMagneticRoundableTargetsForChildren(actual, expectedTargets)
    }

    @Test
    fun findMagneticTargets_forBottomChild_createsEligibleTargets() {
        val childrenNumber = 5
    fun findMagneticRoundableTargets_forBottomChild_createsEligibleTargets() {
        val childrenNumber = 7
        val children = kosmos.createRowGroup(childrenNumber).childrenContainer

        // WHEN the view swiped is the last one in the container
        val swiped = children.attachedChildren[childrenNumber - 1]

        // THEN the neighboring views become targets, with the swiped view at the middle and nulls
        // to the right
        // THEN the neighboring views become targets, with the swiped view at the middle and empty
        // targets to the right
        val actual =
            notificationTargetsHelper()
                .findMagneticTargets(
                .findMagneticRoundableTargets(
                    viewSwiped = swiped,
                    stackScrollLayout = stackScrollLayout,
                    sectionsManager,
                    totalMagneticTargets = 5,
                    numTargets = childrenNumber - 2, // Account for the roundable boundaries
                )
        val expectedRows =
        val expectedTargets =
            listOf(
                children.attachedChildren[childrenNumber - 3],
                children.attachedChildren[childrenNumber - 2],
                swiped,
                null,
                null,
                children.attachedChildren[childrenNumber - 4].toMagneticRoundableTarget(
                    useMagneticListener = false
                ),
                children.attachedChildren[childrenNumber - 3].toMagneticRoundableTarget(),
                children.attachedChildren[childrenNumber - 2].toMagneticRoundableTarget(),
                swiped.toMagneticRoundableTarget(),
                MagneticRoundableTarget.Empty,
                MagneticRoundableTarget.Empty,
                MagneticRoundableTarget.Empty,
            )
        assertMagneticTargetsForChildren(actual, expectedRows)
        assertMagneticRoundableTargetsForChildren(actual, expectedTargets)
    }

    @Test
    fun findMagneticTargets_doesNotCrossSectionAtTop() {
        val childrenNumber = 5
    fun findMagneticRoundableTargets_doesNotCrossSectionAtTop() {
        val childrenNumber = 7
        val children = kosmos.createRowGroup(childrenNumber).childrenContainer

        // WHEN the second child is swiped and the first one begins a new section
        val swiped = children.attachedChildren[1]
        whenever(sectionsManager.beginsSection(swiped, children.attachedChildren[0])).then { true }

        // THEN the neighboring views become targets, with the swiped view at the middle and nulls
        // to the left since the top view relative to swiped begins a new section
        // THEN the neighboring views become targets, with the swiped view at the middle and empty
        // targets to the left (since the top view relative to swiped begins a new section), except
        // For the notification header wrapper as a roundable
        val actual =
            notificationTargetsHelper()
                .findMagneticTargets(
                .findMagneticRoundableTargets(
                    viewSwiped = swiped,
                    stackScrollLayout = stackScrollLayout,
                    sectionsManager,
                    totalMagneticTargets = 5,
                    numTargets = childrenNumber - 2, // Account for roundable boundaries
                )
        val expectedTargets =
            listOf(
                MagneticRoundableTarget.Empty,
                MagneticRoundableTarget.Empty,
                MagneticRoundableTarget(children.notificationHeaderWrapper, null),
                swiped.toMagneticRoundableTarget(),
                children.attachedChildren[2].toMagneticRoundableTarget(),
                children.attachedChildren[3].toMagneticRoundableTarget(),
                children.attachedChildren[4].toMagneticRoundableTarget(useMagneticListener = false),
            )
        val expectedRows =
            listOf(null, null, swiped, children.attachedChildren[2], children.attachedChildren[3])
        assertMagneticTargetsForChildren(actual, expectedRows)
        assertMagneticRoundableTargetsForChildren(actual, expectedTargets)
    }

    @Test
    fun findMagneticTargets_doesNotCrossSectionAtBottom() {
        val childrenNumber = 5
    fun findMagneticRoundableTargets_doesNotCrossSectionAtBottom() {
        val childrenNumber = 7
        val children = kosmos.createRowGroup(childrenNumber).childrenContainer

        // WHEN the fourth child is swiped and the last one begins a new section
        val swiped = children.attachedChildren[3]
        whenever(sectionsManager.beginsSection(children.attachedChildren[4], swiped)).then { true }

        // THEN the neighboring views become targets, with the swiped view at the middle and nulls
        // to the right since the bottom view relative to swiped begins a new section
        // THEN the neighboring views become targets, with the swiped view at the middle and empty
        // targets to the right (since the bottom view relative to swiped begins a new section)
        val actual =
            notificationTargetsHelper()
                .findMagneticTargets(
                .findMagneticRoundableTargets(
                    viewSwiped = swiped,
                    stackScrollLayout = stackScrollLayout,
                    sectionsManager,
                    totalMagneticTargets = 5,
                    numTargets = childrenNumber - 2, // Account for roundable boundaries
                )
        val expectedRows =
            listOf(children.attachedChildren[1], children.attachedChildren[2], swiped, null, null)
        assertMagneticTargetsForChildren(actual, expectedRows)
        val expectTargets =
            listOf(
                children.attachedChildren[0].toMagneticRoundableTarget(useMagneticListener = false),
                children.attachedChildren[1].toMagneticRoundableTarget(),
                children.attachedChildren[2].toMagneticRoundableTarget(),
                swiped.toMagneticRoundableTarget(),
                MagneticRoundableTarget.Empty,
                MagneticRoundableTarget.Empty,
                MagneticRoundableTarget.Empty,
            )
        assertMagneticRoundableTargetsForChildren(actual, expectTargets)
    }

    private fun assertMagneticTargetsForChildren(
        targets: List<MagneticRowListener?>,
        children: List<ExpandableNotificationRow?>,
    private fun assertMagneticRoundableTargetsForChildren(
        current: List<MagneticRoundableTarget>,
        expected: List<MagneticRoundableTarget>,
    ) {
        assertThat(targets.size).isEqualTo(children.size)
        targets.forEachIndexed { i, target ->
            assertThat(target).isEqualTo(children[i]?.magneticRowListener)
        assertThat(current.size).isEqualTo(expected.size)
        current.zip(expected).forEach { (currentTarget, expectedTarget) ->
            assertThat(currentTarget.roundable).isEqualTo(expectedTarget.roundable)
            assertThat(currentTarget.magneticRowListener)
                .isEqualTo(expectedTarget.magneticRowListener)
        }
    }

    private fun ExpandableNotificationRow.toMagneticRoundableTarget(
        useMagneticListener: Boolean = true
    ): MagneticRoundableTarget =
        MagneticRoundableTarget(
            roundable = this,
            magneticRowListener =
                if (useMagneticListener) {
                    this.magneticRowListener
                } else {
                    null
                },
        )
}
+21 −0
Original line number Diff line number Diff line
/*
 * 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.systemui.statusbar.notification

data class TopBottomRoundness(val topRoundness: Float = 0f, val bottomRoundness: Float = 0f) {
    constructor(roundness: Float) : this(roundness, roundness)
}
+48 −19
Original line number Diff line number Diff line
@@ -22,6 +22,8 @@ import androidx.dynamicanimation.animation.SpringForce
import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.notification.Roundable
import com.android.systemui.statusbar.notification.TopBottomRoundness
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.NotificationRowLogger
import com.android.systemui.util.time.SystemClock
@@ -65,8 +67,6 @@ constructor(
        SpringForce().setStiffness(DETACH_STIFFNESS).setDampingRatio(DETACH_DAMPING_RATIO)
    private val snapForce =
        SpringForce().setStiffness(SNAP_BACK_STIFFNESS).setDampingRatio(SNAP_BACK_DAMPING_RATIO)
    private val attachForce =
        SpringForce().setStiffness(ATTACH_STIFFNESS).setDampingRatio(ATTACH_DAMPING_RATIO)

    // Multiplier applied to the translation of a row while swiped
    val swipedRowMultiplier =
@@ -82,6 +82,18 @@ constructor(
    // (see SystemClock.elapsedRealtime)
    private var lastVibrationTime = 0L

    private val detachedRoundnessSet =
        List(ROUNDNESS_MULTIPLIERS.size) { index ->
            when (index) {
                ROUNDNESS_MULTIPLIERS.size / 2 - 1 ->
                    TopBottomRoundness(topRoundness = 0f, bottomRoundness = 1f)
                ROUNDNESS_MULTIPLIERS.size / 2 -> TopBottomRoundness(roundness = 1f)
                ROUNDNESS_MULTIPLIERS.size / 2 + 1 ->
                    TopBottomRoundness(topRoundness = 1f, bottomRoundness = 0f)
                else -> TopBottomRoundness(roundness = 0f)
            }
        }

    override fun setInfoProvider(
        swipeInfoProvider: MagneticNotificationRowManager.SwipeInfoProvider?
    ) {
@@ -115,24 +127,24 @@ constructor(
        stackScrollLayout: NotificationStackScrollLayout,
        sectionsManager: NotificationSectionsManager,
    ) {
        // Update roundable targets
        notificationRoundnessManager.clear()
        val currentRoundableTargets =
            notificationTargetsHelper.findRoundableTargets(
        // All targets
        val newTargets =
            notificationTargetsHelper.findMagneticRoundableTargets(
                expandableNotificationRow,
                stackScrollLayout,
                sectionsManager,
                MAGNETIC_TRANSLATION_MULTIPLIERS.size,
            )
        notificationRoundnessManager.setRoundableTargets(currentRoundableTargets)

        // Update magnetic targets
        // Update roundable targets
        notificationRoundnessManager.clear()
        notificationRoundnessManager.setRoundableTargets(newTargets.map { it.roundable })

        val newListeners =
            notificationTargetsHelper.findMagneticTargets(
                expandableNotificationRow,
                stackScrollLayout,
                sectionsManager,
                MAGNETIC_TRANSLATION_MULTIPLIERS.size,
            )
            newTargets
                // Remove the roundable boundaries
                .filterIndexed { i, _ -> i > 0 && i < newTargets.size - 1 }
                .map { it.magneticRowListener }
        newListeners.forEach {
            if (currentMagneticListeners.contains(it)) {
                it?.cancelMagneticAnimations()
@@ -181,8 +193,18 @@ constructor(

    private fun updateRoundness(translation: Float, animate: Boolean = false) {
        val normalizedTranslation = abs(swipedRowMultiplier * translation) / magneticDetachThreshold
        val cappedRoundness = normalizedTranslation.coerceIn(0f, MAX_PRE_DETACH_ROUNDNESS)
        val roundnessSet =
            ROUNDNESS_MULTIPLIERS.mapIndexed { i, multiplier ->
                val roundness = multiplier * cappedRoundness
                when (i) {
                    0 -> TopBottomRoundness(bottomRoundness = roundness)
                    ROUNDNESS_MULTIPLIERS.size - 1 -> TopBottomRoundness(topRoundness = roundness)
                    else -> TopBottomRoundness(roundness)
                }
            }
        notificationRoundnessManager.setRoundnessForAffectedViews(
            /* roundness */ normalizedTranslation.coerceIn(0f, MAX_PRE_DETACH_ROUNDNESS),
            /* roundnessSet */ roundnessSet,
            animate,
        )
    }
@@ -285,7 +307,7 @@ constructor(
            startVelocity = direction * abs(velocity),
        )
        notificationRoundnessManager.setRoundnessForAffectedViews(
            /* roundness */ 1f,
            /* roundnessSet */ detachedRoundnessSet,
            /* animate */ true,
        )
        playThresholdHaptics()
@@ -403,10 +425,10 @@ constructor(
    private fun ExpandableNotificationRow.isSwipedTarget(): Boolean =
        magneticRowListener == currentMagneticListeners.swipedListener()

    private fun NotificationRoundnessManager.clear() = setViewsAffectedBySwipe(null, null, null)
    private fun NotificationRoundnessManager.clear() = setViewsAffectedBySwipe(listOf())

    private fun NotificationRoundnessManager.setRoundableTargets(targets: RoundableTargets) =
        setViewsAffectedBySwipe(targets.before, targets.swiped, targets.after)
    private fun NotificationRoundnessManager.setRoundableTargets(targets: List<Roundable?>) =
        setViewsAffectedBySwipe(targets)

    /**
     * A class to estimate the direction of a gesture translations with a moving average.
@@ -487,6 +509,13 @@ constructor(
         */
        private val MAGNETIC_TRANSLATION_MULTIPLIERS = listOf(0.04f, 0.12f, 0.5f, 0.12f, 0.04f)

        /**
         * Multipliers applied to roundable targets. Their structure mimic that of
         * [MAGNETIC_TRANSLATION_MULTIPLIERS] but are only used to modify the roundness of current
         * targets.
         */
        private val ROUNDNESS_MULTIPLIERS = listOf(0.5f, 0.7f, 0.9f, 1.0f, 0.9f, 0.7f, 0.5f)

        const val MAGNETIC_REDUCTION = 0.65f

        /** Spring parameters for physics animators */
+49 −50
Original line number Diff line number Diff line
@@ -23,10 +23,13 @@ import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.statusbar.notification.Roundable;
import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.TopBottomRoundness;
import com.android.systemui.statusbar.notification.row.ExpandableView;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

import javax.inject.Inject;

@@ -44,14 +47,13 @@ public class NotificationRoundnessManager implements Dumpable {
    private boolean mRoundForPulsingViews;
    private boolean mIsClearAllInProgress;

    private ExpandableView mSwipedView = null;
    private Roundable mViewBeforeSwipedView = null;
    private Roundable mViewAfterSwipedView = null;
    private List<Roundable> mCurrentRoundables;

    @Inject
    NotificationRoundnessManager(DumpManager dumpManager) {
        mDumpManager = dumpManager;
        mDumpManager.registerDumpable(TAG, this);
        mCurrentRoundables = new ArrayList<>();
    }

    @Override
@@ -61,69 +63,65 @@ public class NotificationRoundnessManager implements Dumpable {
    }

    public boolean isViewAffectedBySwipe(ExpandableView expandableView) {
        return expandableView != null
                && (expandableView == mSwipedView
                || expandableView == mViewBeforeSwipedView
                || expandableView == mViewAfterSwipedView);
        return expandableView != null && mCurrentRoundables.contains(expandableView);
    }

    void setViewsAffectedBySwipe(
            Roundable viewBefore,
            ExpandableView viewSwiped,
            Roundable viewAfter) {
    void setViewsAffectedBySwipe(List<Roundable> newViews) {
        // This method caches a new set of current View targets and reset the roundness of the old
        // View targets (if any) to 0f.
        HashSet<Roundable> oldViews = new HashSet<>();
        if (mViewBeforeSwipedView != null) oldViews.add(mViewBeforeSwipedView);
        if (mSwipedView != null) oldViews.add(mSwipedView);
        if (mViewAfterSwipedView != null) oldViews.add(mViewAfterSwipedView);

        mViewBeforeSwipedView = viewBefore;
        if (viewBefore != null) {
            oldViews.remove(viewBefore);
        }
        // Make a copy of the current views
        List<Roundable> oldViews = new ArrayList<>(mCurrentRoundables);

        mSwipedView = viewSwiped;
        if (viewSwiped != null) {
            oldViews.remove(viewSwiped);
        // From the old set, mark any view that is also contained in the new set as null. The old
        // set will be used to reset roundness but we don't want to modify views that are present on
        // both.
        for (int i = 0; i < oldViews.size(); i++) {
            if (newViews.contains(oldViews.get(i))) {
                oldViews.set(i, null);
            }

        mViewAfterSwipedView = viewAfter;
        if (viewAfter != null) {
            oldViews.remove(viewAfter);
        }

        // After setting the current Views, reset the views that are still present in the set.
        // Reset roundness of the remaining old views
        for (Roundable oldView : oldViews) {
            if (oldView != null) {
                oldView.requestRoundnessReset(DISMISS_ANIMATION);
            }
        }

    void setRoundnessForAffectedViews(float roundness) {
        if (mViewBeforeSwipedView != null) {
            mViewBeforeSwipedView.requestBottomRoundness(roundness, DISMISS_ANIMATION);
        // Replace the current set of views
        mCurrentRoundables = newViews;
    }

        if (mSwipedView != null) {
            mSwipedView.requestRoundness(roundness, roundness, DISMISS_ANIMATION);
    void setViewsAffectedBySwipe(
            Roundable viewBefore,
            ExpandableView viewSwiped,
            Roundable viewAfter) {
        List<Roundable> newViews = new ArrayList<>();
        newViews.add(viewBefore);
        newViews.add(viewSwiped);
        newViews.add(viewAfter);
        setViewsAffectedBySwipe(newViews);
    }

        if (mViewAfterSwipedView != null) {
            mViewAfterSwipedView.requestTopRoundness(roundness, DISMISS_ANIMATION);
    void setRoundnessForAffectedViews(float roundness) {
        for (Roundable affected : mCurrentRoundables) {
            if (affected != null) {
                affected.requestRoundness(roundness, roundness, DISMISS_ANIMATION);
            }
        }

    void setRoundnessForAffectedViews(float roundness, boolean animate) {
        if (mViewBeforeSwipedView != null) {
            mViewBeforeSwipedView.requestBottomRoundness(roundness, DISMISS_ANIMATION, animate);
    }

        if (mSwipedView != null) {
            mSwipedView.requestRoundness(roundness, roundness, DISMISS_ANIMATION, animate);
        }
    void setRoundnessForAffectedViews(List<TopBottomRoundness> roundnessSet, boolean animate) {
        if (roundnessSet.size() != mCurrentRoundables.size()) return;

        if (mViewAfterSwipedView != null) {
            mViewAfterSwipedView.requestTopRoundness(roundness, DISMISS_ANIMATION, animate);
        for (int i = 0; i < roundnessSet.size(); i++) {
            Roundable roundable = mCurrentRoundables.get(i);
            if (roundable != null) {
                TopBottomRoundness roundnessConfig = roundnessSet.get(i);
                roundable.requestRoundness(roundnessConfig.getTopRoundness(),
                        roundnessConfig.getBottomRoundness(), DISMISS_ANIMATION, animate);
            }
        }
    }

@@ -163,6 +161,7 @@ public class NotificationRoundnessManager implements Dumpable {
    }

    public boolean isSwipedViewSet() {
        return mSwipedView != null;
        return !mCurrentRoundables.isEmpty()
                && mCurrentRoundables.get(mCurrentRoundables.size() / 2) != null;
    }
}
+143 −50

File changed.

Preview size limit exceeded, changes collapsed.