From ecca8eacbcc1dca821fedf0d5a92cbce925dd0a3 Mon Sep 17 00:00:00 2001 From: Vinit Nayak Date: Wed, 29 Mar 2023 22:03:34 -0700 Subject: [PATCH] Default to using SplitSelectSource drawable if TaskView icon drawable is null * Alternative solution could be to set onTaskViewVisibilityChanged(true) for the taskView that is about to be dismissed so it loads it's taskIcon/thumbnail back from the cache * However, that does still leave us open to race conditions (even though we can be reasonably confident the icon is probably in the cache) * Also made other changes to allow already public fields on some classes to be mockable for unit testing Fixes: 275267738 Test: Tested with fullscreen task at end of overview, GroupedTaskView at end of overview, Initiating split from home, Initiating split from overview actions, Initiating split from overview app icon Change-Id: Ic9059c93c07b90f61c9f418d5d36d6ba201ff96a --- .../uioverrides/QuickstepLauncher.java | 2 +- .../util/SplitAnimationController.kt | 28 ++- .../util/SplitAnimationControllerTest.kt | 165 ++++++++++++++++++ .../util/SplitConfigurationOptions.java | 12 +- 4 files changed, 198 insertions(+), 9 deletions(-) create mode 100644 quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index 5e227034be..7318298bef 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -639,7 +639,7 @@ public class QuickstepLauncher extends Launcher { PendingAnimation anim = new PendingAnimation(TABLET_HOME_TO_SPLIT.getDuration()); RectF startingTaskRect = new RectF(); final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView(this, - source.view, null /* thumbnail */, source.drawable, startingTaskRect); + source.getView(), null /* thumbnail */, source.getDrawable(), startingTaskRect); floatingTaskView.setAlpha(1); floatingTaskView.addStagingAnimation(anim, startingTaskRect, tempRect, false /* fadeWithThumbnail */, true /* isStagedTask */); diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt index 6dd67def47..b76fe5cb03 100644 --- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt +++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt @@ -24,6 +24,7 @@ import android.view.View import com.android.launcher3.DeviceProfile import com.android.launcher3.anim.PendingAnimation import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource +import com.android.quickstep.views.IconView import com.android.quickstep.views.TaskThumbnailView import com.android.quickstep.views.TaskView import com.android.quickstep.views.TaskView.TaskIdAttributeContainer @@ -52,21 +53,22 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC * depending on the state of the surface from which the split was initiated */ fun getFirstAnimInitViews(taskViewSupplier: Supplier, - splitSelectSourceSupplier: Supplier) + splitSelectSourceSupplier: Supplier) : SplitAnimInitProps { + val splitSelectSource = splitSelectSourceSupplier.get() if (!splitSelectStateController.isAnimateCurrentTaskDismissal) { // Initiating from home - val splitSelectSource = splitSelectSourceSupplier.get() - return SplitAnimInitProps(splitSelectSource.view, originalBitmap = null, + return SplitAnimInitProps(splitSelectSource!!.view, originalBitmap = null, splitSelectSource.drawable, fadeWithThumbnail = false, isStagedTask = true, iconView = null) } else if (splitSelectStateController.isDismissingFromSplitPair) { // Initiating split from overview, but on a split pair val taskView = taskViewSupplier.get() for (container : TaskIdAttributeContainer in taskView.taskIdAttributeContainers) { - if (container.task.key.id == splitSelectStateController.initialTaskId) { + if (container.task.getKey().getId() == splitSelectStateController.initialTaskId) { + val drawable = getDrawable(container.iconView, splitSelectSource) return SplitAnimInitProps(container.thumbnailView, - container.thumbnailView.thumbnail, container.iconView.drawable!!, + container.thumbnailView.thumbnail, drawable!!, fadeWithThumbnail = true, isStagedTask = true, iconView = container.iconView ) @@ -77,13 +79,27 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC } else { // Initiating split from overview on fullscreen task TaskView val taskView = taskViewSupplier.get() + val drawable = getDrawable(taskView.iconView, splitSelectSource) return SplitAnimInitProps(taskView.thumbnail, taskView.thumbnail.thumbnail, - taskView.iconView.drawable!!, fadeWithThumbnail = true, isStagedTask = true, + drawable!!, fadeWithThumbnail = true, isStagedTask = true, taskView.iconView ) } } + /** + * Returns the drawable that's provided in iconView, however if that + * is null it falls back to the drawable that's in splitSelectSource. + * TaskView's icon drawable can be null if the TaskView is scrolled far enough off screen + * @return [Drawable] + */ + fun getDrawable(iconView: IconView, splitSelectSource: SplitSelectSource?) : Drawable? { + if (iconView.drawable == null && splitSelectSource != null) { + return splitSelectSource.drawable + } + return iconView.drawable + } + /** * When selecting first app from split pair, second app's thumbnail remains. This animates * the second thumbnail by expanding it to take up the full taskViewWidth/Height and overlaying diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt new file mode 100644 index 0000000000..7e07b813a7 --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt @@ -0,0 +1,165 @@ +/* + * 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. + * + */ + +package com.android.quickstep.util + +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.view.View +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.quickstep.views.GroupedTaskView +import com.android.quickstep.views.IconView +import com.android.quickstep.views.TaskThumbnailView +import com.android.quickstep.views.TaskView +import com.android.quickstep.views.TaskView.TaskIdAttributeContainer +import com.android.systemui.shared.recents.model.Task +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidJUnit4::class) +class SplitAnimationControllerTest { + + private val taskId = 9 + + @Mock lateinit var mockSplitSelectStateController: SplitSelectStateController + // TaskView + @Mock lateinit var mockTaskView: TaskView + @Mock lateinit var mockThumbnailView: TaskThumbnailView + @Mock lateinit var mockBitmap: Bitmap + @Mock lateinit var mockIconView: IconView + @Mock lateinit var mockTaskViewDrawable: Drawable + // GroupedTaskView + @Mock lateinit var mockGroupedTaskView: GroupedTaskView + @Mock lateinit var mockTask: Task + @Mock lateinit var mockTaskKey: Task.TaskKey + @Mock lateinit var mockTaskIdAttributeContainer: TaskIdAttributeContainer + + // SplitSelectSource + @Mock lateinit var splitSelectSource: SplitConfigurationOptions.SplitSelectSource + @Mock lateinit var mockSplitSourceDrawable: Drawable + @Mock lateinit var mockSplitSourceView: View + + lateinit var splitAnimationController: SplitAnimationController + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + whenever(mockTaskView.thumbnail).thenReturn(mockThumbnailView) + whenever(mockThumbnailView.thumbnail).thenReturn(mockBitmap) + whenever(mockTaskView.iconView).thenReturn(mockIconView) + whenever(mockIconView.drawable).thenReturn(mockTaskViewDrawable) + + whenever(splitSelectSource.drawable).thenReturn(mockSplitSourceDrawable) + whenever(splitSelectSource.view).thenReturn(mockSplitSourceView) + + splitAnimationController = SplitAnimationController(mockSplitSelectStateController) + } + + @Test + fun getFirstAnimInitViews_nullTaskViewIcon_useSplitSourceIcon() { + // Hit fullscreen task dismissal state + whenever(mockSplitSelectStateController.isAnimateCurrentTaskDismissal).thenReturn(true) + whenever(mockSplitSelectStateController.isDismissingFromSplitPair).thenReturn(false) + + // Missing taskView icon + whenever(mockIconView.drawable).thenReturn(null) + + val splitAnimInitProps : SplitAnimationController.Companion.SplitAnimInitProps = + splitAnimationController.getFirstAnimInitViews( + { mockTaskView }, { splitSelectSource }) + + assertEquals("Did not fallback to use splitSource icon drawable", + mockSplitSourceDrawable, splitAnimInitProps.iconDrawable) + } + + @Test + fun getFirstAnimInitViews_validTaskViewIcon_useTaskViewIcon() { + // Hit fullscreen task dismissal state + whenever(mockSplitSelectStateController.isAnimateCurrentTaskDismissal).thenReturn(true) + whenever(mockSplitSelectStateController.isDismissingFromSplitPair).thenReturn(false) + + val splitAnimInitProps : SplitAnimationController.Companion.SplitAnimInitProps = + splitAnimationController.getFirstAnimInitViews( + { mockTaskView }, { splitSelectSource }) + + assertEquals("Did not use taskView icon drawable", mockTaskViewDrawable, + splitAnimInitProps.iconDrawable) + } + + @Test + fun getFirstAnimInitViews_validTaskViewNullSplitSource_useTaskViewIcon() { + // Hit fullscreen task dismissal state + whenever(mockSplitSelectStateController.isAnimateCurrentTaskDismissal).thenReturn(true) + whenever(mockSplitSelectStateController.isDismissingFromSplitPair).thenReturn(false) + + // Set split source to null + whenever(splitSelectSource.drawable).thenReturn(null) + + val splitAnimInitProps : SplitAnimationController.Companion.SplitAnimInitProps = + splitAnimationController.getFirstAnimInitViews( + { mockTaskView }, { splitSelectSource }) + + assertEquals("Did not use taskView icon drawable", mockTaskViewDrawable, + splitAnimInitProps.iconDrawable) + } + + @Test + fun getFirstAnimInitViews_nullTaskViewValidSplitSource_noTaskDismissal() { + // Hit initiating split from home + whenever(mockSplitSelectStateController.isAnimateCurrentTaskDismissal).thenReturn(false) + whenever(mockSplitSelectStateController.isDismissingFromSplitPair).thenReturn(false) + + val splitAnimInitProps : SplitAnimationController.Companion.SplitAnimInitProps = + splitAnimationController.getFirstAnimInitViews( + { mockTaskView }, { splitSelectSource }) + + assertEquals("Did not use splitSource icon drawable", mockSplitSourceDrawable, + splitAnimInitProps.iconDrawable) + } + + @Test + fun getFirstAnimInitViews_nullTaskViewValidSplitSource_groupedTaskView() { + // Hit groupedTaskView dismissal + whenever(mockSplitSelectStateController.isAnimateCurrentTaskDismissal).thenReturn(true) + whenever(mockSplitSelectStateController.isDismissingFromSplitPair).thenReturn(true) + + // Remove icon view from GroupedTaskView + whenever(mockIconView.drawable).thenReturn(null) + + whenever(mockTaskIdAttributeContainer.task).thenReturn(mockTask) + whenever(mockTaskIdAttributeContainer.iconView).thenReturn(mockIconView) + whenever(mockTaskIdAttributeContainer.thumbnailView).thenReturn(mockThumbnailView) + whenever(mockTask.getKey()).thenReturn(mockTaskKey) + whenever(mockTaskKey.getId()).thenReturn(taskId) + whenever(mockSplitSelectStateController.initialTaskId).thenReturn(taskId) + whenever(mockGroupedTaskView.taskIdAttributeContainers) + .thenReturn(Array(1) { mockTaskIdAttributeContainer }) + val splitAnimInitProps : SplitAnimationController.Companion.SplitAnimInitProps = + splitAnimationController.getFirstAnimInitViews( + { mockGroupedTaskView }, { splitSelectSource }) + + assertEquals("Did not use splitSource icon drawable", mockSplitSourceDrawable, + splitAnimInitProps.iconDrawable) + } +} \ No newline at end of file diff --git a/src/com/android/launcher3/util/SplitConfigurationOptions.java b/src/com/android/launcher3/util/SplitConfigurationOptions.java index 8c5e78295b..1ae43d000b 100644 --- a/src/com/android/launcher3/util/SplitConfigurationOptions.java +++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java @@ -200,8 +200,8 @@ public final class SplitConfigurationOptions { /** Keep in sync w/ ActivityTaskManager#INVALID_TASK_ID (unreference-able) */ private static final int INVALID_TASK_ID = -1; - public final View view; - public final Drawable drawable; + private View view; + private Drawable drawable; public final Intent intent; public final SplitPositionOption position; public final ItemInfo itemInfo; @@ -224,5 +224,13 @@ public final class SplitConfigurationOptions { this.itemInfo = itemInfo; this.splitEvent = splitEvent; } + + public Drawable getDrawable() { + return drawable; + } + + public View getView() { + return view; + } } }