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

Commit ecca8eac authored by Vinit Nayak's avatar Vinit Nayak
Browse files

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
parent 90259a6d
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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 */);
+22 −6
Original line number Diff line number Diff line
@@ -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<TaskView>,
                              splitSelectSourceSupplier: Supplier<SplitSelectSource>)
                              splitSelectSourceSupplier: Supplier<SplitSelectSource?>)
            : 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
+165 −0
Original line number Diff line number Diff line
/*
 *  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
+10 −2
Original line number Diff line number Diff line
@@ -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;
        }
    }
}