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

Commit b99cc5b4 authored by Chet Haase's avatar Chet Haase Committed by Steven Terrell
Browse files

Pause animators when app is not visible

Because animators are not tied to the lifecycle of any UI
elements, it is possible for an app to go into the background
and for the animators to continue running. Ideally, the app would
track the lifecycle of the activity/etc and pause or disable the
animators, but it is common for this to not happen, causing the
animators to continue spinning when the app does not need them.
The animators are not causing as much work as for a foreground
activity (since they do not cause any re-rendering), but they cause
work nonetheless by keeping Choreographer awake to continue pulsing
frames.

The ideal fix would be to introduce new API for animators that
tied them to lifecycle concepts (View, Activity, etc). But that kind
of fix would only be available for future versions of the platform,
and does not address existing app code. A workaround for the current
situation is to address the most egregious problems; infinite animators
running on backgrounded apps.

The fix here is exactly that: when an app's visible surface (either an
activity or, for Wallpapers, a WallpaperService) is backgrounded,
a request is sent to pause animators for that surface. When that surface
comes to the foreground, a request is sent to resume those animators.
Since all animators are handled on the same thread for the same process,
in AnimationHandler, we should only ever pause animators when *all*
surfaces for a process are not visible (and resume them when *any*
surface becomes visible). Also, to mitigate any issues with thrashing
animator state for apps which become only transiently backgrounded,
we delay pausing for some time.

Bug: 228598053
Bug: 233391022
Test: new AnimatorLeak CTS test, plus manual testing for activities
    and wallpapers

Change-Id: I8b9f841cc80babb972244c724968a5c085a06b69
Merged-In: I8b9f841cc80babb972244c724968a5c085a06b69
parent 7ac3be0b
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -96,6 +96,11 @@ package android.accessibilityservice {

package android.animation {

  public abstract class Animator implements java.lang.Cloneable {
    method public static long getBackgroundPauseDelay();
    method public static void setBackgroundPauseDelay(long);
  }

  public class ValueAnimator extends android.animation.Animator {
    method @MainThread public static void setDurationScale(@FloatRange(from=0) float);
  }
+109 −1
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package android.animation;

import android.os.SystemClock;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.view.Choreographer;

import java.util.ArrayList;
@@ -35,10 +37,13 @@ import java.util.ArrayList;
 * @hide
 */
public class AnimationHandler {

    private static final String TAG = "AnimationHandler";
    private static final boolean LOCAL_LOGV = true;

    /**
     * Internal per-thread collections used to avoid set collisions as animations start and end
     * while being processed.
     * @hide
     */
    private final ArrayMap<AnimationFrameCallback, Long> mDelayedCallbackStartTime =
            new ArrayMap<>();
@@ -48,6 +53,26 @@ public class AnimationHandler {
            new ArrayList<>();
    private AnimationFrameCallbackProvider mProvider;

    /**
     * This paused list is used to store animators forcibly paused when the activity
     * went into the background (to avoid unnecessary background processing work).
     * These animators should be resume()'d when the activity returns to the foreground.
     */
    private final ArrayList<Animator> mPausedAnimators = new ArrayList<>();

    /**
     * This structure is used to store the currently active objects (ViewRootImpls or
     * WallpaperService.Engines) in the process. Each of these objects sends a request to
     * AnimationHandler when it goes into the background (request to pause) or foreground
     * (request to resume). Because all animators are managed by AnimationHandler on the same
     * thread, it should only ever pause animators when *all* requestors are in the background.
     * This list tracks the background/foreground state of all requestors and only ever
     * pauses animators when all items are in the background (false). To simplify, we only ever
     * store visible (foreground) requestors; if the set size reaches zero, there are no
     * objects in the foreground and it is time to pause animators.
     */
    private final ArraySet<Object> mAnimatorRequestors = new ArraySet<>();

    private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
@@ -68,6 +93,89 @@ public class AnimationHandler {
        return sAnimatorHandler.get();
    }


    /**
     * This is called when a window goes away. We should remove
     * it from the requestors list to ensure that we are counting requests correctly and not
     * tracking obsolete+enabled requestors.
     */
    public static void removeRequestor(Object requestor) {
        getInstance().removeRequestorImpl(requestor);
    }

    private void removeRequestorImpl(Object requestor) {
        // Also request disablement, in case that requestor was the sole object keeping
        // animators un-paused
        requestAnimatorsEnabled(false, requestor);
        mAnimatorRequestors.remove(requestor);
        if (LOCAL_LOGV) {
            Log.v(TAG, "removeRequestorImpl for " + requestor);
            for (int i = 0; i < mAnimatorRequestors.size(); ++i) {
                Log.v(TAG, "animatorRequesters " + i + " = " + mAnimatorRequestors.valueAt(i));
            }
        }
    }

    /**
     * This method is called from ViewRootImpl or WallpaperService when either a window is no
     * longer visible (enable == false) or when a window becomes visible (enable == true).
     * If animators are not properly disabled when activities are backgrounded, it can lead to
     * unnecessary processing, particularly for infinite animators, as the system will continue
     * to pulse timing events even though the results are not visible. As a workaround, we
     * pause all un-paused infinite animators, and resume them when any window in the process
     * becomes visible.
     */
    public static void requestAnimatorsEnabled(boolean enable, Object requestor) {
        getInstance().requestAnimatorsEnabledImpl(enable, requestor);
    }

    private void requestAnimatorsEnabledImpl(boolean enable, Object requestor) {
        boolean wasEmpty = mAnimatorRequestors.isEmpty();
        if (enable) {
            mAnimatorRequestors.add(requestor);
        } else {
            mAnimatorRequestors.remove(requestor);
        }
        boolean isEmpty = mAnimatorRequestors.isEmpty();
        if (wasEmpty != isEmpty) {
            // only paused/resume animators if there was a visibility change
            if (!isEmpty) {
                // If any requestors are enabled, resume currently paused animators
                Choreographer.getInstance().removeFrameCallback(mPauser);
                for (int i = mPausedAnimators.size() - 1; i >= 0; --i) {
                    mPausedAnimators.get(i).resume();
                }
                mPausedAnimators.clear();
            } else {
                // Wait before pausing to avoid thrashing animator state for temporary backgrounding
                Choreographer.getInstance().postFrameCallbackDelayed(mPauser,
                        Animator.getBackgroundPauseDelay());
            }
        }
        if (LOCAL_LOGV) {
            Log.v(TAG, enable ? "enable" : "disable" + " animators for " + requestor);
            for (int i = 0; i < mAnimatorRequestors.size(); ++i) {
                Log.v(TAG, "animatorRequesters " + i + " = " + mAnimatorRequestors.valueAt(i));
            }
        }
    }

    private Choreographer.FrameCallback mPauser = frameTimeNanos -> {
        if (mAnimatorRequestors.size() > 0) {
            // something enabled animators since this callback was scheduled - bail
            return;
        }
        for (int i = 0; i < mAnimationCallbacks.size(); ++i) {
            Animator animator = ((Animator) mAnimationCallbacks.get(i));
            if (animator != null
                    && animator.getTotalDuration() == Animator.DURATION_INFINITE
                    && !animator.isPaused()) {
                mPausedAnimators.add(animator);
                animator.pause();
            }
        }
    };

    /**
     * By default, the Choreographer is used to provide timing for frame callbacks. A custom
     * provider can be used here to provide different timing pulse.
+29 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package android.animation;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.pm.ActivityInfo.Config;
import android.content.res.ConstantState;
@@ -63,6 +64,34 @@ public abstract class Animator implements Cloneable {
     */
    private AnimatorConstantState mConstantState;

    /**
     * backing field for backgroundPauseDelay property. This could be simply a hardcoded
     * value in AnimationHandler, but it is useful to be able to change the value in tests.
     */
    private static long sBackgroundPauseDelay = 10000;

    /**
     * Sets the duration for delaying pausing animators when apps go into the background.
     * Used by AnimationHandler when requested to pause animators.
     *
     * @hide
     */
    @TestApi
    public static void setBackgroundPauseDelay(long value) {
        sBackgroundPauseDelay = value;
    }

    /**
     * Gets the duration for delaying pausing animators when apps go into the background.
     * Used by AnimationHandler when requested to pause animators.
     *
     * @hide
     */
    @TestApi
    public static long getBackgroundPauseDelay() {
        return sBackgroundPauseDelay;
    }

    /**
     * Starts this animation. If the animation has a nonzero startDelay, the animation will start
     * running after that delay elapses. A non-delayed animation will have its initial
+6 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import static android.view.View.SYSTEM_UI_FLAG_VISIBLE;
import static android.view.ViewRootImpl.LOCAL_LAYOUT;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;

import android.animation.AnimationHandler;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
@@ -1516,6 +1517,8 @@ public abstract class WallpaperService extends Service {
                mVisible = visible;
                reportVisibility();
                if (mReportedVisible) processLocalColors(mPendingXOffset, mPendingXOffsetStep);
            } else {
                AnimationHandler.requestAnimatorsEnabled(visible, this);
            }
        }

@@ -1544,6 +1547,7 @@ public abstract class WallpaperService extends Service {
                        if (DEBUG) Log.v(TAG, "Freezing wallpaper after visibility update");
                        freeze();
                    }
                    AnimationHandler.requestAnimatorsEnabled(visible, this);
                }
            }
        }
@@ -2072,6 +2076,8 @@ public abstract class WallpaperService extends Service {
                return;
            }

            AnimationHandler.removeRequestor(this);

            mDestroyed = true;

            if (mIWallpaperEngine.mDisplayManager != null) {
+5 −0
Original line number Diff line number Diff line
@@ -90,6 +90,7 @@ import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodCl
import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER;

import android.Manifest;
import android.animation.AnimationHandler;
import android.animation.LayoutTransition;
import android.annotation.AnyThread;
import android.annotation.NonNull;
@@ -1363,6 +1364,8 @@ public final class ViewRootImpl implements ViewParent,
                mFirstInputStage = nativePreImeStage;
                mFirstPostImeInputStage = earlyPostImeStage;
                mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix;

                AnimationHandler.requestAnimatorsEnabled(mAppVisible, this);
            }
        }
    }
@@ -1708,6 +1711,7 @@ public final class ViewRootImpl implements ViewParent,
            if (!mAppVisible) {
                WindowManagerGlobal.trimForeground();
            }
            AnimationHandler.requestAnimatorsEnabled(mAppVisible, this);
        }
    }

@@ -8477,6 +8481,7 @@ public final class ViewRootImpl implements ViewParent,
            mInsetsController.onControlsChanged(null);

            mAdded = false;
            AnimationHandler.removeRequestor(this);
        }
        WindowManagerGlobal.getInstance().doRemoveView(this);
    }