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

Commit ff4f1624 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Support TaskOverlay with new TaskThumbnailView" into main

parents 50586a37 58e8109d
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