Merge "Ensure that launched Views implement LaunchableView" into tm-qpr-dev

This commit is contained in:
Jordan Demeulenaere
2023-01-30 11:27:29 +00:00
committed by Android (Google) Code Review
21 changed files with 242 additions and 93 deletions

View File

@@ -41,6 +41,7 @@ import androidx.annotation.BinderThread
import androidx.annotation.UiThread
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.policy.ScreenDecorationsUtils
import java.lang.IllegalArgumentException
import kotlin.math.roundToInt
private const val TAG = "ActivityLaunchAnimator"
@@ -338,13 +339,24 @@ class ActivityLaunchAnimator(
* Return a [Controller] that will animate and expand [view] into the opening window.
*
* Important: The view must be attached to a [ViewGroup] when calling this function and
* during the animation. For safety, this method will return null when it is not.
* during the animation. For safety, this method will return null when it is not. The
* view must also implement [LaunchableView], otherwise this method will throw.
*
* Note: The background of [view] should be a (rounded) rectangle so that it can be
* properly animated.
*/
@JvmStatic
fun fromView(view: View, cujType: Int? = null): Controller? {
// Make sure the View we launch from implements LaunchableView to avoid visibility
// issues.
if (view !is LaunchableView) {
throw IllegalArgumentException(
"An ActivityLaunchAnimator.Controller was created from a View that does " +
"not implement LaunchableView. This can lead to subtle bugs where the" +
" visibility of the View we are launching from is not what we expected."
)
}
if (view.parent !is ViewGroup) {
Log.e(
TAG,

View File

@@ -40,6 +40,7 @@ import com.android.systemui.animation.back.BackAnimationSpec
import com.android.systemui.animation.back.applyTo
import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi
import com.android.systemui.animation.back.onBackAnimationCallbackFrom
import java.lang.IllegalArgumentException
import kotlin.math.roundToInt
private const val TAG = "DialogLaunchAnimator"
@@ -157,12 +158,23 @@ constructor(
* Create a [Controller] that can animate [source] to and from a dialog.
*
* Important: The view must be attached to a [ViewGroup] when calling this function and
* during the animation. For safety, this method will return null when it is not.
* during the animation. For safety, this method will return null when it is not. The
* view must also implement [LaunchableView], otherwise this method will throw.
*
* Note: The background of [view] should be a (rounded) rectangle so that it can be
* properly animated.
*/
fun fromView(source: View, cuj: DialogCuj? = null): Controller? {
// Make sure the View we launch from implements LaunchableView to avoid visibility
// issues.
if (source !is LaunchableView) {
throw IllegalArgumentException(
"A DialogLaunchAnimator.Controller was created from a View that does not " +
"implement LaunchableView. This can lead to subtle bugs where the " +
"visibility of the View we are launching from is not what we expected."
)
}
if (source.parent !is ViewGroup) {
Log.e(
TAG,
@@ -249,23 +261,6 @@ constructor(
}
?: controller
if (
animatedParent == null &&
controller is ViewDialogLaunchAnimatorController &&
controller.source !is LaunchableView
) {
// Make sure the View we launch from implements LaunchableView to avoid visibility
// issues. Given that we don't own dialog decorViews so we can't enforce it for launches
// from a dialog.
// TODO(b/243636422): Throw instead of logging to enforce this.
Log.w(
TAG,
"A dialog was launched from a View that does not implement LaunchableView. This " +
"can lead to subtle bugs where the visibility of the View we are " +
"launching from is not what we expected."
)
}
// Make sure we don't run the launch animation from the same source twice at the same time.
if (openedDialogs.any { it.controller.sourceIdentity == controller.sourceIdentity }) {
Log.e(
@@ -613,10 +608,16 @@ private class AnimatedDialog(
}
// Animate that view with the background. Throw if we didn't find one, because
// otherwise
// it's not clear what we should animate.
// otherwise it's not clear what we should animate.
if (viewGroupWithBackground == null) {
error("Unable to find ViewGroup with background")
}
if (viewGroupWithBackground !is LaunchableView) {
error("The animated ViewGroup with background must implement LaunchableView")
}
viewGroupWithBackground
?: throw IllegalStateException("Unable to find ViewGroup with background")
} else {
// We will make the dialog window (and therefore its DecorView) fullscreen to make
// it possible to animate outside its bounds.
@@ -639,7 +640,7 @@ private class AnimatedDialog(
FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
)
val dialogContentWithBackground = FrameLayout(dialog.context)
val dialogContentWithBackground = LaunchableFrameLayout(dialog.context)
dialogContentWithBackground.background = decorView.background
// Make the window background transparent. Note that setting the window (or
@@ -720,7 +721,10 @@ private class AnimatedDialog(
// Make the background view invisible until we start the animation. We use the transition
// visibility like GhostView does so that we don't mess up with the accessibility tree (see
// b/204944038#comment17).
// b/204944038#comment17). Given that this background implements LaunchableView, we call
// setShouldBlockVisibilityChanges() early so that the current visibility (VISIBLE) is
// restored at the end of the animation.
dialogContentWithBackground.setShouldBlockVisibilityChanges(true)
dialogContentWithBackground.setTransitionVisibility(View.INVISIBLE)
// Make sure the dialog is visible instantly and does not do any window animation.

View File

@@ -34,6 +34,7 @@ import android.view.ViewGroup
import android.view.ViewGroupOverlay
import android.widget.FrameLayout
import com.android.internal.jank.InteractionJankMonitor
import java.lang.IllegalArgumentException
import java.util.LinkedList
import kotlin.math.min
import kotlin.math.roundToInt
@@ -46,7 +47,8 @@ private const val TAG = "GhostedViewLaunchAnimatorController"
* of the ghosted view.
*
* Important: [ghostedView] must be attached to a [ViewGroup] when calling this function and during
* the animation.
* the animation. It must also implement [LaunchableView], otherwise an exception will be thrown
* during this controller instantiation.
*
* Note: Avoid instantiating this directly and call [ActivityLaunchAnimator.Controller.fromView]
* whenever possible instead.
@@ -101,6 +103,15 @@ constructor(
private val background: Drawable?
init {
// Make sure the View we launch from implements LaunchableView to avoid visibility issues.
if (ghostedView !is LaunchableView) {
throw IllegalArgumentException(
"A GhostedViewLaunchAnimatorController was created from a View that does not " +
"implement LaunchableView. This can lead to subtle bugs where the visibility " +
"of the View we are launching from is not what we expected."
)
}
/** Find the first view with a background in [view] and its children. */
fun findBackground(view: View): Drawable? {
if (view.background != null) {

View File

@@ -0,0 +1,53 @@
/*
* Copyright (C) 2022 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.animation
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
/** A [FrameLayout] that also implements [LaunchableView]. */
open class LaunchableFrameLayout : FrameLayout, LaunchableView {
private val delegate =
LaunchableViewDelegate(
this,
superSetVisibility = { super.setVisibility(it) },
)
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes)
override fun setShouldBlockVisibilityChanges(block: Boolean) {
delegate.setShouldBlockVisibilityChanges(block)
}
override fun setVisibility(visibility: Int) {
delegate.setVisibility(visibility)
}
}

View File

@@ -25,7 +25,7 @@ import com.android.internal.jank.InteractionJankMonitor
/** A [DialogLaunchAnimator.Controller] that can animate a [View] from/to a dialog. */
class ViewDialogLaunchAnimatorController
internal constructor(
internal val source: View,
private val source: View,
override val cuj: DialogCuj?,
) : DialogLaunchAnimator.Controller {
override val viewRoot: ViewRootImpl?

View File

@@ -20,7 +20,7 @@
android:layout_width="wrap_content"
android:paddingVertical="@dimen/dream_overlay_complication_home_controls_padding">
<ImageView
<com.android.systemui.common.ui.view.LaunchableImageView
android:id="@+id/home_controls_chip"
android:layout_height="@dimen/keyguard_affordance_fixed_height"
android:layout_width="@dimen/keyguard_affordance_fixed_width"

View File

@@ -33,7 +33,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_weight="1">
<androidx.constraintlayout.widget.ConstraintLayout
<com.android.systemui.common.ui.view.LaunchableConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@android:id/list"
@@ -55,6 +55,6 @@
app:flow_horizontalGap="@dimen/global_actions_lite_padding"
app:flow_verticalGap="@dimen/global_actions_lite_padding"
app:flow_horizontalStyle="packed"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.android.systemui.common.ui.view.LaunchableConstraintLayout>
</com.android.systemui.globalactions.GlobalActionsLayoutLite>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -24,7 +24,7 @@
android:layout_gravity="end">
<!-- We add a background behind the UserAvatarView with the same color and with a circular shape
so that this view can be expanded into a Dialog or an Activity. -->
<FrameLayout
<com.android.systemui.animation.LaunchableFrameLayout
android:id="@+id/kg_multi_user_avatar_with_background"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -42,5 +42,5 @@
systemui:framePadding="0dp"
systemui:frameWidth="0dp">
</com.android.systemui.statusbar.phone.UserAvatarView>
</FrameLayout>
</com.android.systemui.animation.LaunchableFrameLayout>
</FrameLayout>

View File

@@ -106,7 +106,7 @@
app:layout_constrainedWidth="true"
app:layout_constraintWidth_min="@dimen/min_clickable_item_size"
app:layout_constraintHeight_min="@dimen/min_clickable_item_size">
<LinearLayout
<com.android.systemui.common.ui.view.LaunchableLinearLayout
android:id="@+id/media_seamless_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -135,7 +135,7 @@
android:textDirection="locale"
android:textSize="12sp"
android:lineHeight="16sp" />
</LinearLayout>
</com.android.systemui.common.ui.view.LaunchableLinearLayout>
</LinearLayout>
<!-- Song name -->

View File

@@ -23,7 +23,7 @@
android:layout_gravity="center_vertical|start"
android:layout_marginStart="5dp"
>
<LinearLayout
<com.android.systemui.common.ui.view.LaunchableLinearLayout
android:id="@+id/ongoing_call_chip_background"
android:layout_width="wrap_content"
android:layout_height="@dimen/ongoing_appops_chip_height"
@@ -55,5 +55,5 @@
android:textColor="?android:attr/colorPrimary"
/>
</LinearLayout>
</com.android.systemui.common.ui.view.LaunchableLinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,55 @@
/*
* Copyright (C) 2022 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.common.ui.view
import android.content.Context
import android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout
import com.android.systemui.animation.LaunchableView
import com.android.systemui.animation.LaunchableViewDelegate
/** A [ConstraintLayout] that also implements [LaunchableView]. */
open class LaunchableConstraintLayout : ConstraintLayout, LaunchableView {
private val delegate =
LaunchableViewDelegate(
this,
superSetVisibility = { super.setVisibility(it) },
)
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes)
override fun setShouldBlockVisibilityChanges(block: Boolean) {
delegate.setShouldBlockVisibilityChanges(block)
}
override fun setVisibility(visibility: Int) {
delegate.setVisibility(visibility)
}
}

View File

@@ -23,7 +23,7 @@ import com.android.systemui.animation.LaunchableView
import com.android.systemui.animation.LaunchableViewDelegate
/** A [LinearLayout] that also implements [LaunchableView]. */
class LaunchableLinearLayout : LinearLayout, LaunchableView {
open class LaunchableLinearLayout : LinearLayout, LaunchableView {
private val delegate =
LaunchableViewDelegate(
this,

View File

@@ -17,11 +17,11 @@ package com.android.systemui.privacy
import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import com.android.settingslib.Utils
import com.android.systemui.R
import com.android.systemui.animation.LaunchableFrameLayout
import com.android.systemui.statusbar.events.BackgroundAnimatableView
class OngoingPrivacyChip @JvmOverloads constructor(
@@ -29,7 +29,7 @@ class OngoingPrivacyChip @JvmOverloads constructor(
attrs: AttributeSet? = null,
defStyleAttrs: Int = 0,
defStyleRes: Int = 0
) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes), BackgroundAnimatableView {
) : LaunchableFrameLayout(context, attrs, defStyleAttrs, defStyleRes), BackgroundAnimatableView {
private var iconMargin = 0
private var iconSize = 0

View File

@@ -20,10 +20,21 @@ import android.content.Context;
import android.util.AttributeSet;
import android.widget.Button;
import com.android.systemui.animation.LaunchableView;
import com.android.systemui.animation.LaunchableViewDelegate;
import kotlin.Unit;
/**
* A Button which doesn't have overlapping drawing commands
*/
public class AlphaOptimizedButton extends Button {
public class AlphaOptimizedButton extends Button implements LaunchableView {
private LaunchableViewDelegate mDelegate = new LaunchableViewDelegate(this,
(visibility) -> {
super.setVisibility(visibility);
return Unit.INSTANCE;
});
public AlphaOptimizedButton(Context context) {
super(context);
}
@@ -45,4 +56,14 @@ public class AlphaOptimizedButton extends Button {
public boolean hasOverlappingRendering() {
return false;
}
@Override
public void setShouldBlockVisibilityChanges(boolean block) {
mDelegate.setShouldBlockVisibilityChanges(block);
}
@Override
public void setVisibility(int visibility) {
mDelegate.setVisibility(visibility);
}
}

View File

@@ -19,14 +19,14 @@ package com.android.systemui.statusbar.phone.userswitcher
import android.content.Context
import android.util.AttributeSet
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.android.systemui.R
import com.android.systemui.common.ui.view.LaunchableLinearLayout
class StatusBarUserSwitcherContainer(
context: Context?,
attrs: AttributeSet?
) : LinearLayout(context, attrs) {
) : LaunchableLinearLayout(context, attrs) {
lateinit var text: TextView
private set
lateinit var avatar: ImageView

View File

@@ -27,6 +27,8 @@ import android.view.ViewTreeObserver
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import com.android.systemui.animation.LaunchableView
import com.android.systemui.animation.LaunchableViewDelegate
import com.android.systemui.statusbar.CrossFadeHelper
/**
@@ -38,7 +40,7 @@ class TransitionLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
) : ConstraintLayout(context, attrs, defStyleAttr), LaunchableView {
private val boundsRect = Rect()
private val originalGoneChildrenSet: MutableSet<Int> = mutableSetOf()
@@ -50,7 +52,11 @@ class TransitionLayout @JvmOverloads constructor(
private var desiredMeasureWidth = 0
private var desiredMeasureHeight = 0
private var transitionVisibility = View.VISIBLE
private val delegate =
LaunchableViewDelegate(
this,
superSetVisibility = { super.setVisibility(it) },
)
/**
* The measured state of this view which is the one we will lay ourselves out with. This
@@ -83,11 +89,12 @@ class TransitionLayout @JvmOverloads constructor(
}
}
override fun setTransitionVisibility(visibility: Int) {
// We store the last transition visibility assigned to this view to restore it later if
// necessary.
super.setTransitionVisibility(visibility)
transitionVisibility = visibility
override fun setShouldBlockVisibilityChanges(block: Boolean) {
delegate.setShouldBlockVisibilityChanges(block)
}
override fun setVisibility(visibility: Int) {
delegate.setVisibility(visibility)
}
override fun onFinishInflate() {
@@ -173,14 +180,6 @@ class TransitionLayout @JvmOverloads constructor(
translationY = currentState.translation.y
CrossFadeHelper.fadeIn(this, currentState.alpha)
// CrossFadeHelper#fadeIn will change this view visibility, which overrides the transition
// visibility. We set the transition visibility again to make sure that this view plays well
// with GhostView, which sets the transition visibility and is used for activity launch
// animations.
if (transitionVisibility != View.VISIBLE) {
setTransitionVisibility(transitionVisibility)
}
}
private fun applyCurrentStateOnPredraw() {

View File

@@ -15,6 +15,7 @@ import android.view.RemoteAnimationAdapter
import android.view.RemoteAnimationTarget
import android.view.SurfaceControl
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -26,6 +27,7 @@ import junit.framework.Assert.assertTrue
import junit.framework.AssertionFailedError
import kotlin.concurrent.thread
import org.junit.After
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -195,6 +197,13 @@ class ActivityLaunchAnimatorTest : SysuiTestCase() {
verify(controller).onLaunchAnimationStart(anyBoolean())
}
@Test
fun creatingControllerFromNormalViewThrows() {
assertThrows(IllegalArgumentException::class.java) {
ActivityLaunchAnimator.Controller.fromView(FrameLayout(mContext))
}
}
private fun fakeWindow(): RemoteAnimationTarget {
val bounds = Rect(10 /* left */, 20 /* top */, 30 /* right */, 40 /* bottom */)
val taskInfo = ActivityManager.RunningTaskInfo()

View File

@@ -26,6 +26,7 @@ import junit.framework.Assert.assertNull
import junit.framework.Assert.assertTrue
import org.junit.After
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -260,6 +261,13 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {
assertThat(touchSurface.visibility).isEqualTo(View.GONE)
}
@Test
fun creatingControllerFromNormalViewThrows() {
assertThrows(IllegalArgumentException::class.java) {
DialogLaunchAnimator.Controller.fromView(FrameLayout(mContext))
}
}
private fun createAndShowDialog(
animator: DialogLaunchAnimator = dialogLaunchAnimator,
): TestDialog {

View File

@@ -16,58 +16,34 @@
package com.android.systemui.animation
import android.graphics.drawable.Drawable
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
import android.view.ViewGroup
import android.view.ViewParent
import android.widget.FrameLayout
import androidx.test.filters.SmallTest
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.SysuiTestCase
import org.junit.Before
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class GhostedViewLaunchAnimatorControllerTest : SysuiTestCase() {
@Mock lateinit var interactionJankMonitor: InteractionJankMonitor
@Mock lateinit var view: View
@Mock lateinit var rootView: ViewGroup
@Mock lateinit var viewParent: ViewParent
@Mock lateinit var drawable: Drawable
lateinit var controller: GhostedViewLaunchAnimatorController
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
whenever(view.rootView).thenReturn(rootView)
whenever(view.background).thenReturn(drawable)
whenever(view.height).thenReturn(0)
whenever(view.width).thenReturn(0)
whenever(view.parent).thenReturn(viewParent)
whenever(view.visibility).thenReturn(View.VISIBLE)
whenever(view.invalidate()).then { /* NO-OP */ }
whenever(view.getLocationOnScreen(any())).then { /* NO-OP */ }
whenever(interactionJankMonitor.begin(any(), anyInt())).thenReturn(true)
whenever(interactionJankMonitor.end(anyInt())).thenReturn(true)
controller = GhostedViewLaunchAnimatorController(view, 0, interactionJankMonitor)
}
@Test
fun animatingOrphanViewDoesNotCrash() {
val state = LaunchAnimator.State(top = 0, bottom = 0, left = 0, right = 0)
val controller = GhostedViewLaunchAnimatorController(LaunchableFrameLayout(mContext))
controller.onIntentStarted(willAnimate = true)
controller.onLaunchAnimationStart(isExpandingFullyAbove = true)
controller.onLaunchAnimationProgress(state, progress = 0f, linearProgress = 0f)
controller.onLaunchAnimationEnd(isExpandingFullyAbove = true)
}
@Test
fun creatingControllerFromNormalViewThrows() {
assertThrows(IllegalArgumentException::class.java) {
GhostedViewLaunchAnimatorController(FrameLayout(mContext))
}
}
}

View File

@@ -31,13 +31,13 @@ import android.content.ComponentName;
import android.content.Context;
import android.testing.AndroidTestingRunner;
import android.view.View;
import android.widget.ImageView;
import androidx.test.filters.SmallTest;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.common.ui.view.LaunchableImageView;
import com.android.systemui.controls.ControlsServiceInfo;
import com.android.systemui.controls.controller.ControlsController;
import com.android.systemui.controls.controller.StructureInfo;
@@ -90,7 +90,7 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase {
private View mView;
@Mock
private ImageView mHomeControlsView;
private LaunchableImageView mHomeControlsView;
@Mock
private ActivityStarter mActivityStarter;

View File

@@ -35,6 +35,7 @@ import android.view.View
import com.android.internal.logging.MetricsLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.animation.LaunchableFrameLayout
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.qs.QSTile
@@ -342,7 +343,7 @@ class CustomTileTest : SysuiTestCase() {
val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext)
tile.qsTile.activityLaunchForClick = pi
tile.handleClick(mock(View::class.java))
tile.handleClick(mock(LaunchableFrameLayout::class.java))
testableLooper.processAllMessages()