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

Commit 58e8109d authored by Alex Chau's avatar Alex Chau
Browse files

Support TaskOverlay with new TaskThumbnailView

- Also migrated getScaledInsets method into TaksOverlay

Bug: 335606129
Test: TaskOverlayViewModelTest
Test: TaskOverlayHelper is not tested because it should be a view-based screenshot test for TaskOverlay, which is currently impossible until we refactor TaskOverlay to MVVM
Flag: com.android.launcher3.enable_refactor_task_thumbnail
Change-Id: I07a8657ff0fe925d8021875310e3ed12a712ba7a
parent 30de7530
Loading
Loading
Loading
Loading
+82 −7
Original line number Diff line number Diff line
@@ -16,18 +16,22 @@

package com.android.quickstep;

import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
import static com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL;
import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import com.android.launcher3.BaseActivity;
@@ -38,6 +42,7 @@ import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.util.ResourceBasedOverride;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.Snackbar;
import com.android.quickstep.task.util.TaskOverlayHelper;
import com.android.quickstep.util.RecentsOrientedState;
import com.android.quickstep.views.DesktopTaskView;
import com.android.quickstep.views.GroupedTaskView;
@@ -128,12 +133,43 @@ public class TaskOverlayFactory implements ResourceBasedOverride {

        private T mActionsView;
        protected ImageActionsApi mImageApi;
        protected TaskOverlayHelper mHelper;

        protected TaskOverlay(TaskContainer taskContainer) {
            mApplicationContext = taskContainer.getTaskView().getContext().getApplicationContext();
            mTaskContainer = taskContainer;
            mImageApi = new ImageActionsApi(
                    mApplicationContext, mTaskContainer::getThumbnail);
            if (enableRefactorTaskThumbnail()) {
                mHelper = new TaskOverlayHelper(mTaskContainer.getTask(), this);
            }
            mImageApi = new ImageActionsApi(mApplicationContext, this::getThumbnail);
        }

        /**
         * Initialize the overlay when a Task is bound to the TaskView.
         */
        public void init() {
            if (enableRefactorTaskThumbnail()) {
                mHelper.init();
            }
        }

        /**
         * Destroy the overlay when the TaskView is recycled.
         */
        public void destroy() {
            if (enableRefactorTaskThumbnail()) {
                mHelper.destroy();
            }
        }

        protected @Nullable Bitmap getThumbnail() {
            return enableRefactorTaskThumbnail() ? mHelper.getEnabledState().getThumbnail()
                    : mTaskContainer.getThumbnailViewDeprecated().getThumbnail();
        }

        protected boolean isRealSnapshot() {
            return enableRefactorTaskThumbnail() ? mHelper.getEnabledState().isRealSnapshot()
                    : mTaskContainer.getThumbnailViewDeprecated().isRealSnapshot();
        }

        protected T getActionsView() {
@@ -151,15 +187,25 @@ public class TaskOverlayFactory implements ResourceBasedOverride {

        /**
         * Called when the current task is interactive for the user
         *
         * @deprecated TODO(b/350931107): Remove this interface once TaskOverlayFactoryGo is updated
         */
        @Deprecated
        public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
                boolean rotated) {
            initOverlay(task, thumbnail.getThumbnail(), matrix, rotated);
        }

        /**
         * Called when the current task is interactive for the user
         */
        public void initOverlay(Task task, @Nullable Bitmap thumbnail, Matrix matrix,
                boolean rotated) {
            getActionsView().updateDisabledFlags(DISABLED_NO_THUMBNAIL, thumbnail == null);

            if (thumbnail != null) {
                getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
                boolean isAllowedByPolicy = mTaskContainer.isRealSnapshot();
                getActionsView().setCallbacks(new OverlayUICallbacksImpl(isAllowedByPolicy, task));
                getActionsView().setCallbacks(new OverlayUICallbacksImpl(isRealSnapshot(), task));
            }
        }

@@ -183,8 +229,8 @@ public class TaskOverlayFactory implements ResourceBasedOverride {
         */
        @SuppressLint("NewApi")
        protected void saveScreenshot(Task task) {
            if (mTaskContainer.isRealSnapshot()) {
                mImageApi.saveScreenshot(mTaskContainer.getThumbnail(),
            if (isRealSnapshot()) {
                mImageApi.saveScreenshot(getThumbnail(),
                        getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key);
            } else {
                showBlockedByPolicyMessage();
@@ -259,7 +305,36 @@ public class TaskOverlayFactory implements ResourceBasedOverride {
         */
        @RequiresApi(api = Build.VERSION_CODES.Q)
        public Insets getTaskSnapshotInsets() {
            return mTaskContainer.getScaledInsets();
            Bitmap thumbnail = getThumbnail();
            if (thumbnail == null) {
                return Insets.NONE;
            }

            RectF bitmapRect = new RectF(
                    0,
                    0,
                    thumbnail.getWidth(),
                    thumbnail.getHeight());
            View snapshotView = mTaskContainer.getSnapshotView();
            RectF viewRect = new RectF(0, 0, snapshotView.getMeasuredWidth(),
                    snapshotView.getMeasuredHeight());

            // The position helper matrix tells us how to transform the bitmap to fit the view, the
            // inverse tells us where the view would be in the bitmaps coordinates. The insets are
            // the difference between the bitmap bounds and the projected view bounds.
            Matrix boundsToBitmapSpace = new Matrix();
            Matrix thumbnailMatrix = enableRefactorTaskThumbnail()
                    ? mHelper.getEnabledState().getThumbnailMatrix()
                    : mTaskContainer.getThumbnailViewDeprecated().getThumbnailMatrix();
            thumbnailMatrix.invert(boundsToBitmapSpace);
            RectF boundsInBitmapSpace = new RectF();
            boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect);

            RecentsViewContainer container = RecentsViewContainer.containerFromContext(
                    getTaskView().getContext());
            int bottomInset = container.getDeviceProfile().isTablet
                    ? Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom) : 0;
            return Insets.of(0, 0, 0, bottomInset);
        }

        /**
+7 −0
Original line number Diff line number Diff line
@@ -24,4 +24,11 @@ class RecentsViewData {

    // This is typically a View concern but it is used to invalidate rendering in other Views
    val scale = MutableStateFlow(1f)

    // Whether the current RecentsView state supports task overlays.
    // TODO(b/331753115): Derive from RecentsView state flow once migrated to MVVM.
    val overlayEnabled = MutableStateFlow(false)

    // The settled set of visible taskIds that is updated after RecentsView scroll settles.
    val settledFullyVisibleTaskIds = MutableStateFlow(emptySet<Int>())
}
+31 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.task.thumbnail

import android.graphics.Bitmap
import android.graphics.Matrix

/** Ui state for [com.android.quickstep.TaskOverlayFactory.TaskOverlay] */
sealed class TaskOverlayUiState {
    data object Disabled : TaskOverlayUiState()

    data class Enabled(
        val isRealSnapshot: Boolean,
        val thumbnail: Bitmap?,
        val thumbnailMatrix: Matrix
    ) : TaskOverlayUiState()
}
+90 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.task.util

import android.util.Log
import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.task.thumbnail.TaskOverlayUiState
import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
import com.android.quickstep.views.RecentsView
import com.android.quickstep.views.RecentsViewContainer
import com.android.systemui.shared.recents.model.Task
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch

/**
 * Helper for [TaskOverlayFactory.TaskOverlay] to interact with [TaskOverlayViewModel], this helper
 * should merge with [TaskOverlayFactory.TaskOverlay] when it's migrated to MVVM.
 */
class TaskOverlayHelper(val task: Task, val overlay: TaskOverlayFactory.TaskOverlay<*>) {
    private lateinit var job: Job
    private var uiState: TaskOverlayUiState = Disabled

    // TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped
    //  to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
    //  This is using a lazy for now because the dependencies cannot be obtained without DI.
    private val taskOverlayViewModel by lazy {
        val recentsView =
            RecentsViewContainer.containerFromContext<RecentsViewContainer>(
                    overlay.taskView.context
                )
                .getOverviewPanel<RecentsView<*, *>>()
        TaskOverlayViewModel(task, recentsView.mRecentsViewData, recentsView.mTasksRepository)
    }

    // TODO(b/331753115): TaskOverlay should listen for state changes and react.
    val enabledState: Enabled
        get() = uiState as Enabled

    fun init() {
        // TODO(b/335396935): This should be changed to TaskView's scope.
        job =
            MainScope().launch {
                taskOverlayViewModel.overlayState.collect {
                    uiState = it
                    if (it is Enabled) {
                        Log.d(
                            TAG,
                            "initOverlay - taskId: ${task.key.id}, thumbnail: ${it.thumbnail}"
                        )
                        overlay.initOverlay(
                            task,
                            it.thumbnail,
                            it.thumbnailMatrix,
                            /* rotated= */ false
                        )
                    } else {
                        Log.d(TAG, "reset - taskId: ${task.key.id}")
                        overlay.reset()
                    }
                }
            }
    }

    fun destroy() {
        job.cancel()
        uiState = Disabled
        overlay.reset()
    }

    companion object {
        private const val TAG = "TaskOverlayHelper"
    }
}
+59 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.task.viewmodel

import android.graphics.Matrix
import com.android.quickstep.recents.data.RecentTasksRepository
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
import com.android.systemui.shared.recents.model.Task
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.map

/** View model for TaskOverlay */
class TaskOverlayViewModel(
    task: Task,
    recentsViewData: RecentsViewData,
    tasksRepository: RecentTasksRepository,
) {
    val overlayState =
        combine(
                recentsViewData.overlayEnabled,
                recentsViewData.settledFullyVisibleTaskIds.map { it.contains(task.key.id) },
                tasksRepository
                    .getTaskDataById(task.key.id)
                    .map { it?.thumbnail }
                    .distinctUntilChangedBy { it?.snapshotId }
            ) { isOverlayEnabled, isFullyVisible, thumbnailData ->
                if (isOverlayEnabled && isFullyVisible) {
                    Enabled(
                        isRealSnapshot = (thumbnailData?.isRealSnapshot ?: false) && !task.isLocked,
                        thumbnailData?.thumbnail,
                        // TODO(b/343101424): Use PreviewPositionHelper, listen from a common source
                        // with
                        //  TaskThumbnailView.
                        Matrix.IDENTITY_MATRIX
                    )
                } else {
                    Disabled
                }
            }
            .distinctUntilChanged()
}
Loading