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

Commit 28784296 authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[Media TTT] Don't use an animated-vector for the loading spinner.

See bug for more context. tl;dr: AnimatedVectorDrawables will pause
their animation when opening the shade (or in other scenarios), so we
need to use a different kind of drawable as a workaround. This CL just
uses a static drawable and animates its rotation using an
`ObjectAnimator`.

Bug: 243983980
Test: `adb shell cmd statusbar media-ttt-chip-sender MyTablet
TRANSFER_TO_RECEIVER_TRIGGERED` -> see new loading spinner. Pull down
the shade and verify the loading spinner keeps spinning. Plug the device
in to charge to see the charging animation and verify the loading
spinner keeps spinning.
Test: _TRIGGERD -> _SUCCEEDED -> Undo ==> verify that the new loading
spinner for the new TRIGGERED state is spinning
Test: atest ChipbarCoordinatorTest

Change-Id: Ic5701bbb241b919211463df6eb576f810ec2ca22
parent d504771e
Loading
Loading
Loading
Loading
+26 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
     Copyright (C) 2023 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="48dp"
    android:height="48dp"
    android:viewportWidth="48"
    android:viewportHeight="48"
    android:tint="?attr/colorControlNormal">
    <path
        android:fillColor="@android:color/white"
        android:pathData="M24,44Q19.8,44 16.15,42.45Q12.5,40.9 9.8,38.2Q7.1,35.5 5.55,31.85Q4,28.2 4,24Q4,19.8 5.55,16.15Q7.1,12.5 9.8,9.8Q12.5,7.1 16.15,5.55Q19.8,4 24,4Q24.6,4 25.05,4.45Q25.5,4.9 25.5,5.5Q25.5,6.1 25.05,6.55Q24.6,7 24,7Q16.95,7 11.975,11.975Q7,16.95 7,24Q7,31.05 11.975,36.025Q16.95,41 24,41Q31.05,41 36.025,36.025Q41,31.05 41,24Q41,23.4 41.45,22.95Q41.9,22.5 42.5,22.5Q43.1,22.5 43.55,22.95Q44,23.4 44,24Q44,28.2 42.45,31.85Q40.9,35.5 38.2,38.2Q35.5,40.9 31.85,42.45Q28.2,44 24,44Z"/>
</vector>
+3 −4
Original line number Diff line number Diff line
@@ -60,14 +60,13 @@
            />

        <!-- At most one of [loading, failure_icon, undo] will be visible at a time. -->
        <ProgressBar
        <ImageView
            android:id="@+id/loading"
            android:indeterminate="true"
            android:layout_width="@dimen/media_ttt_status_icon_size"
            android:layout_height="@dimen/media_ttt_status_icon_size"
            android:layout_marginStart="@dimen/media_ttt_last_item_start_margin"
            android:indeterminateTint="?androidprv:attr/colorAccentPrimaryVariant"
            style="?android:attr/progressBarStyleSmall"
            android:src="@drawable/ic_progress_activity"
            android:tint="?androidprv:attr/colorAccentPrimaryVariant"
            android:alpha="0.0"
            />

+58 −5
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.temporarydisplay.chipbar

import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Rect
import android.os.PowerManager
@@ -27,11 +29,14 @@ import android.view.View.ACCESSIBILITY_LIVE_REGION_NONE
import android.view.ViewGroup
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.IdRes
import androidx.annotation.VisibleForTesting
import com.android.internal.widget.CachingIconView
import com.android.systemui.Gefingerpoken
import com.android.systemui.R
import com.android.systemui.animation.Interpolators
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
import com.android.systemui.common.shared.model.Text.Companion.loadText
@@ -101,6 +106,15 @@ constructor(

    private lateinit var parent: ChipbarRootView

    /** The current loading information, or null we're not currently loading. */
    @VisibleForTesting
    internal var loadingDetails: LoadingDetails? = null
        private set(value) {
            // Always cancel the old one before updating
            field?.animator?.cancel()
            field = value
        }

    override val windowLayoutParams =
        commonWindowLayoutParams.apply { gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) }

@@ -143,8 +157,22 @@ constructor(

        // ---- End item ----
        // Loading
        currentView.requireViewById<View>(R.id.loading).visibility =
            (newInfo.endItem == ChipbarEndItem.Loading).visibleIfTrue()
        val isLoading = newInfo.endItem == ChipbarEndItem.Loading
        val loadingView = currentView.requireViewById<ImageView>(R.id.loading)
        loadingView.visibility = isLoading.visibleIfTrue()

        if (isLoading) {
            val currentLoadingDetails = loadingDetails
            // Since there can be multiple chipbars, we need to check if the loading view is the
            // same and possibly re-start the loading animation on the new view.
            if (currentLoadingDetails == null || currentLoadingDetails.loadingView != loadingView) {
                val newDetails = createLoadingDetails(loadingView)
                newDetails.animator.start()
                loadingDetails = newDetails
            }
        } else {
            loadingDetails = null
        }

        // Error
        currentView.requireViewById<View>(R.id.error).visibility =
@@ -223,12 +251,17 @@ constructor(
    override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
        val innerView = view.getInnerView()
        innerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE
        val removed = chipbarAnimator.animateViewOut(innerView, onAnimationEnd)

        val fullEndRunnable = Runnable {
            loadingDetails = null
            onAnimationEnd.run()
        }
        val removed = chipbarAnimator.animateViewOut(innerView, fullEndRunnable)
        // If the view doesn't get animated, the [onAnimationEnd] runnable won't get run. So, just
        // run it immediately.
        if (!removed) {
            logger.logAnimateOutFailure()
            onAnimationEnd.run()
            fullEndRunnable.run()
        }

        updateGestureListening()
@@ -269,7 +302,7 @@ constructor(
    }

    private fun ViewGroup.getInnerView(): ViewGroup {
        return requireViewById(R.id.chipbar_inner)
        return this.requireViewById(R.id.chipbar_inner)
    }

    override fun getTouchableRegion(view: View, outRect: Rect) {
@@ -283,8 +316,28 @@ constructor(
            View.GONE
        }
    }

    private fun createLoadingDetails(loadingView: View): LoadingDetails {
        // Ideally, we would use a <ProgressBar> view, which would automatically handle the loading
        // spinner rotation for us. However, due to b/243983980, the ProgressBar animation
        // unexpectedly pauses when SysUI starts another window. ObjectAnimator is a workaround that
        // won't pause.
        val animator =
            ObjectAnimator.ofFloat(loadingView, View.ROTATION, 0f, 360f).apply {
                duration = LOADING_ANIMATION_DURATION_MS
                repeatCount = ValueAnimator.INFINITE
                interpolator = Interpolators.LINEAR
            }
        return LoadingDetails(loadingView, animator)
    }

    internal data class LoadingDetails(
        val loadingView: View,
        val animator: ObjectAnimator,
    )
}

@IdRes private val INFO_TAG = R.id.tag_chipbar_info
private const val SWIPE_UP_GESTURE_REASON = "SWIPE_UP_GESTURE_DETECTED"
private const val TAG = "ChipbarCoordinator"
private const val LOADING_ANIMATION_DURATION_MS = 1000L
+100 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
import android.widget.ImageView
import android.widget.TextView
import androidx.core.animation.doOnCancel
import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.systemui.R
@@ -360,6 +361,105 @@ class ChipbarCoordinatorTest : SysuiTestCase() {
        assertThat(isClicked).isTrue()
    }

    @Test
    fun displayView_loading_animationStarted() {
        underTest.displayView(
            createChipbarInfo(
                Icon.Resource(R.id.check_box, null),
                Text.Loaded("text"),
                endItem = ChipbarEndItem.Loading,
            )
        )

        assertThat(underTest.loadingDetails!!.animator.isStarted).isTrue()
    }

    @Test
    fun displayView_notLoading_noAnimation() {
        underTest.displayView(
            createChipbarInfo(
                Icon.Resource(R.id.check_box, null),
                Text.Loaded("text"),
                endItem = ChipbarEndItem.Error,
            )
        )

        assertThat(underTest.loadingDetails).isNull()
    }

    @Test
    fun displayView_loadingThenNotLoading_animationStopped() {
        underTest.displayView(
            createChipbarInfo(
                Icon.Resource(R.id.check_box, null),
                Text.Loaded("text"),
                endItem = ChipbarEndItem.Loading,
            )
        )

        val animator = underTest.loadingDetails!!.animator
        var cancelled = false
        animator.doOnCancel { cancelled = true }

        underTest.displayView(
            createChipbarInfo(
                Icon.Resource(R.id.check_box, null),
                Text.Loaded("text"),
                endItem = ChipbarEndItem.Button(Text.Loaded("button")) {},
            )
        )

        assertThat(cancelled).isTrue()
        assertThat(underTest.loadingDetails).isNull()
    }

    @Test
    fun displayView_loadingThenHideView_animationStopped() {
        underTest.displayView(
            createChipbarInfo(
                Icon.Resource(R.id.check_box, null),
                Text.Loaded("text"),
                endItem = ChipbarEndItem.Loading,
            )
        )

        val animator = underTest.loadingDetails!!.animator
        var cancelled = false
        animator.doOnCancel { cancelled = true }

        underTest.removeView(DEVICE_ID, "TestReason")

        assertThat(cancelled).isTrue()
        assertThat(underTest.loadingDetails).isNull()
    }

    @Test
    fun displayView_loadingThenNewLoading_animationStaysTheSame() {
        underTest.displayView(
            createChipbarInfo(
                Icon.Resource(R.id.check_box, null),
                Text.Loaded("text"),
                endItem = ChipbarEndItem.Loading,
            )
        )

        val animator = underTest.loadingDetails!!.animator
        var cancelled = false
        animator.doOnCancel { cancelled = true }

        underTest.displayView(
            createChipbarInfo(
                Icon.Resource(R.id.check_box, null),
                Text.Loaded("new text"),
                endItem = ChipbarEndItem.Loading,
            )
        )

        assertThat(underTest.loadingDetails!!.animator).isEqualTo(animator)
        assertThat(underTest.loadingDetails!!.animator.isStarted).isTrue()
        assertThat(cancelled).isFalse()
    }

    @Test
    fun displayView_vibrationEffect_doubleClickEffect() {
        underTest.displayView(