diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java index 67feb6a6b8..9f81124a26 100644 --- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java +++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java @@ -56,7 +56,6 @@ import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVIT import static com.android.launcher3.testing.shared.TestProtocol.WALLPAPER_OPEN_ANIMATION_FINISHED_MESSAGE; import static com.android.launcher3.util.DisplayController.isTransientTaskbar; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; -import static com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR; import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs; import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION; @@ -79,7 +78,6 @@ import android.app.role.RoleManager; import android.content.ComponentName; import android.content.Context; import android.content.res.Resources; -import android.database.ContentObserver; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Point; @@ -94,7 +92,6 @@ import android.os.Looper; import android.os.SystemProperties; import android.os.UserHandle; import android.provider.Settings; -import android.provider.Settings.Global; import android.util.Pair; import android.util.Size; import android.view.CrossWindowBlurListeners; @@ -249,16 +246,6 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener // {@link TaskRestartedDuringLaunchListener}, and remove them on activity destroy. private final List mRegisteredTaskStackChangeListener = new ArrayList<>(); - private final ContentObserver mAnimationRemovalObserver = new ContentObserver( - ORDERED_BG_EXECUTOR.getHandler()) { - @Override - public void onChange(boolean selfChange) { - mAreAnimationsEnabled = Global.getFloat(mLauncher.getContentResolver(), - Global.ANIMATOR_DURATION_SCALE, 1f) > 0 - || Global.getFloat(mLauncher.getContentResolver(), - Global.TRANSITION_ANIMATION_SCALE, 1f) > 0; - } - }; private DeviceProfile mDeviceProfile; @@ -287,7 +274,6 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener // Pairs of window starting type and starting window background color for starting tasks // Will never be larger than MAX_NUM_TASKS private LinkedHashMap> mTaskStartParams; - private boolean mAreAnimationsEnabled = true; private final Interpolator mOpeningXInterpolator; private final Interpolator mOpeningInterpolator; @@ -298,7 +284,6 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener mHandler = new Handler(Looper.getMainLooper()); mDeviceProfile = mLauncher.getDeviceProfile(); mBackAnimationController = new LauncherBackAnimationController(mLauncher, this); - checkAndMonitorIfAnimationsAreEnabled(); Resources res = mLauncher.getResources(); mClosingWindowTransY = res.getDimensionPixelSize(R.dimen.closing_window_trans_y); @@ -1220,8 +1205,6 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener unregisterRemoteTransitions(); mLauncher.removeOnDeviceProfileChangeListener(this); SystemUiProxy.INSTANCE.get(mLauncher).setStartingWindowListener(null); - ORDERED_BG_EXECUTOR.execute(() -> mLauncher.getContentResolver() - .unregisterContentObserver(mAnimationRemovalObserver)); if (BuildConfig.IS_STUDIO_BUILD && !mRegisteredTaskStackChangeListener.isEmpty()) { throw new IllegalStateException("Failed to run onEndCallback created from" + " getActivityLaunchOptions()"); @@ -1275,17 +1258,6 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } } - private void checkAndMonitorIfAnimationsAreEnabled() { - ORDERED_BG_EXECUTOR.execute(() -> { - mAnimationRemovalObserver.onChange(true); - mLauncher.getContentResolver().registerContentObserver(Global.getUriFor( - Global.ANIMATOR_DURATION_SCALE), false, mAnimationRemovalObserver); - mLauncher.getContentResolver().registerContentObserver(Global.getUriFor( - Global.TRANSITION_ANIMATION_SCALE), false, mAnimationRemovalObserver); - - }); - } - private boolean launcherIsATargetWithMode(RemoteAnimationTarget[] targets, int mode) { for (RemoteAnimationTarget target : targets) { if (target.mode == mode && target.taskInfo != null @@ -1421,7 +1393,8 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener (LauncherAppWidgetHostView) launcherView, targetRect, windowSize, mDeviceProfile.isMultiWindowMode ? 0 : getWindowCornerRadius(mLauncher), isTransluscent, fallbackBackgroundColor); - } else if (launcherView != null && mAreAnimationsEnabled) { + } else if (launcherView != null && !RemoveAnimationSettingsTracker.INSTANCE.get( + mLauncher).isRemoveAnimationEnabled()) { floatingIconView = getFloatingIconView(mLauncher, launcherView, null, mLauncher.getTaskbarUIController() == null ? null diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt index b4ffb744df..5d1288c106 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt @@ -39,6 +39,7 @@ import androidx.core.view.updateLayoutParams import com.airbnb.lottie.LottieAnimationView import com.android.launcher3.LauncherPrefs import com.android.launcher3.R +import com.android.launcher3.RemoveAnimationSettingsTracker import com.android.launcher3.Utilities import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN @@ -128,6 +129,26 @@ open class TaskbarEduTooltipController(context: Context) : activityContext.dragLayer.post { maybeShowSearchEdu() } } + /** + * Turns off auto play of lottie animations if user has opted to remove animation else attaches + * click listener to allow user to play or pause animations. + */ + fun handleEduAnimations(animationViews: List) { + for (animationView in animationViews) { + if ( + RemoveAnimationSettingsTracker.INSTANCE.get(animationView.context) + .isRemoveAnimationEnabled() + ) { + animationView.pauseAnimation() + } else { + animationView.setOnClickListener { + if (animationView.isAnimating) animationView.pauseAnimation() + else animationView.playAnimation() + } + } + } + } + /** Shows swipe EDU tooltip if it is the current [tooltipStep]. */ fun maybeShowSwipeEdu() { if ( @@ -145,7 +166,9 @@ open class TaskbarEduTooltipController(context: Context) : requireViewById(R.id.taskbar_edu_title), TypefaceUtils.FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED, ) - requireViewById(R.id.swipe_animation).supportLightTheme() + val swipeAnimation = requireViewById(R.id.swipe_animation) + swipeAnimation.supportLightTheme() + handleEduAnimations(listOf(swipeAnimation)) show() } } @@ -174,6 +197,7 @@ open class TaskbarEduTooltipController(context: Context) : splitscreenAnim.supportLightTheme() suggestionsAnim.supportLightTheme() pinningAnim.supportLightTheme() + handleEduAnimations(listOf(splitscreenAnim, suggestionsAnim, pinningAnim)) if (DisplayController.isTransientTaskbar(activityContext)) { splitscreenAnim.setAnimation(R.raw.taskbar_edu_splitscreen_transient) suggestionsAnim.setAnimation(R.raw.taskbar_edu_suggestions_transient) @@ -249,9 +273,6 @@ open class TaskbarEduTooltipController(context: Context) : tooltip?.run { allowTouchDismissal = true - requireViewById(R.id.standalone_pinning_animation) - .supportLightTheme() - TypefaceUtils.setTypeface( requireViewById(R.id.taskbar_edu_title), TypefaceUtils.FONT_FAMILY_HEADLINE_SMALL_EMPHASIZED, @@ -261,6 +282,10 @@ open class TaskbarEduTooltipController(context: Context) : TypefaceUtils.FONT_FAMILY_BODY_MEDIUM_BASELINE, ) + val pinningAnim = + requireViewById(R.id.standalone_pinning_animation) + pinningAnim.supportLightTheme() + handleEduAnimations(listOf(pinningAnim)) updateLayoutParams { if (DisplayController.isTransientTaskbar(activityContext)) { bottomMargin += activityContext.deviceProfile.taskbarHeight @@ -304,7 +329,9 @@ open class TaskbarEduTooltipController(context: Context) : inflateTooltip(R.layout.taskbar_edu_search) tooltip?.run { allowTouchDismissal = true - requireViewById(R.id.search_edu_animation).supportLightTheme() + val searchEdu = requireViewById(R.id.search_edu_animation) + searchEdu.supportLightTheme() + handleEduAnimations(listOf(searchEdu)) val eduSubtitle: TextView = requireViewById(R.id.search_edu_text) TypefaceUtils.setTypeface( diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index 5072e37314..d6abb56286 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -17,6 +17,7 @@ package com.android.launcher3; import static com.android.app.animation.Interpolators.SCROLL; +import static com.android.launcher3.RemoveAnimationSettingsTracker.WINDOW_ANIMATION_SCALE_URI; import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled; import static com.android.launcher3.compat.AccessibilityManagerCompat.isObservedEventType; import static com.android.launcher3.testing.shared.TestProtocol.SCROLL_FINISHED_MESSAGE; @@ -33,7 +34,6 @@ import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.os.Bundle; -import android.provider.Settings; import android.util.AttributeSet; import android.util.Log; import android.view.InputDevice; @@ -1756,8 +1756,8 @@ public abstract class PagedView extends ViewGrou } if (FeatureFlags.IS_STUDIO_BUILD && !Utilities.isRunningInTestHarness()) { - duration *= Settings.Global.getFloat(getContext().getContentResolver(), - Settings.Global.WINDOW_ANIMATION_SCALE, 1); + duration *= RemoveAnimationSettingsTracker.INSTANCE.get(getContext()).getValue( + WINDOW_ANIMATION_SCALE_URI); } whichPage = validateNewPage(whichPage); diff --git a/src/com/android/launcher3/RemoveAnimationSettingsTracker.kt b/src/com/android/launcher3/RemoveAnimationSettingsTracker.kt new file mode 100644 index 0000000000..dbc04f1613 --- /dev/null +++ b/src/com/android/launcher3/RemoveAnimationSettingsTracker.kt @@ -0,0 +1,110 @@ +/* + * 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.launcher3 + +import android.content.Context +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import android.provider.Settings.Global.ANIMATOR_DURATION_SCALE +import android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE +import android.provider.Settings.Global.WINDOW_ANIMATION_SCALE +import com.android.launcher3.dagger.ApplicationContext +import com.android.launcher3.dagger.LauncherAppComponent +import com.android.launcher3.dagger.LauncherAppSingleton +import com.android.launcher3.util.DaggerSingletonObject +import com.android.launcher3.util.DaggerSingletonTracker +import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +/** Tracker Class for when user turns on/off remove animation setting. */ +@LauncherAppSingleton +class RemoveAnimationSettingsTracker +@Inject +constructor(@ApplicationContext val context: Context, tracker: DaggerSingletonTracker) : + ContentObserver(Handler(Looper.getMainLooper())) { + + private val contentResolver = context.contentResolver + + /** Caches the last seen value for registered keys. */ + private val cache: MutableMap = ConcurrentHashMap() + + init { + UI_HELPER_EXECUTOR.execute { + contentResolver.registerContentObserver(WINDOW_ANIMATION_SCALE_URI, false, this) + contentResolver.registerContentObserver(TRANSITION_ANIMATION_SCALE_URI, false, this) + contentResolver.registerContentObserver(ANIMATOR_DURATION_SCALE_URI, false, this) + } + + tracker.addCloseable { + UI_HELPER_EXECUTOR.execute { contentResolver.unregisterContentObserver(this) } + } + } + + /** + * Returns the value for this classes key from the cache. If not in cache, will call + * [updateValue] to fetch. + */ + fun getValue(uri: Uri): Float { + return getValue(uri, 1f) + } + + /** + * Returns the value for this classes key from the cache. If not in cache, will call + * [getValueFromSettingsGlobal] to fetch. + */ + private fun getValue(uri: Uri, defaultValue: Float): Float { + return cache.computeIfAbsent(uri) { getValueFromSettingsGlobal(uri, defaultValue) } + } + + /** Returns if user has opted into having no animation on their device. */ + fun isRemoveAnimationEnabled(): Boolean { + return getValue(WINDOW_ANIMATION_SCALE_URI) == 0f && + getValue(TRANSITION_ANIMATION_SCALE_URI) == 0f && + getValue(ANIMATOR_DURATION_SCALE_URI) == 0f + } + + override fun onChange(selfChange: Boolean, uri: Uri?) { + if (uri == null) return + updateValue(uri) + } + + private fun getValueFromSettingsGlobal(uri: Uri, defaultValue: Float = 1f): Float { + return Settings.Global.getFloat(contentResolver, uri.lastPathSegment, defaultValue) + } + + private fun updateValue(uri: Uri, defaultValue: Float = 1f) { + val newValue = getValueFromSettingsGlobal(uri, defaultValue) + cache[uri] = newValue + } + + companion object { + @JvmField + val INSTANCE = + DaggerSingletonObject(LauncherAppComponent::getRemoveAnimationSettingsTracker) + @JvmField + val WINDOW_ANIMATION_SCALE_URI: Uri = Settings.Global.getUriFor(WINDOW_ANIMATION_SCALE) + @JvmField + val TRANSITION_ANIMATION_SCALE_URI: Uri = + Settings.Global.getUriFor(TRANSITION_ANIMATION_SCALE) + @JvmField + val ANIMATOR_DURATION_SCALE_URI: Uri = Settings.Global.getUriFor(ANIMATOR_DURATION_SCALE) + } +} diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java index 249a2141b7..150761f75c 100644 --- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java +++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java @@ -23,6 +23,7 @@ import androidx.annotation.Nullable; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherPrefs; +import com.android.launcher3.RemoveAnimationSettingsTracker; import com.android.launcher3.graphics.GridCustomizationsProxy; import com.android.launcher3.graphics.ThemeManager; import com.android.launcher3.icons.LauncherIcons.IconPool; @@ -80,8 +81,8 @@ public interface LauncherBaseAppComponent { LockedUserState getLockedUserState(); InvariantDeviceProfile getIDP(); IconPool getIconPool(); + RemoveAnimationSettingsTracker getRemoveAnimationSettingsTracker(); LauncherAppState getLauncherAppState(); - GridCustomizationsProxy getGridCustomizationsProxy(); /** Builder for LauncherBaseAppComponent. */