/* * 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.wm.shell.desktopmode import android.graphics.Rect import android.graphics.Region import android.util.ArrayMap import android.util.ArraySet import android.util.SparseArray import android.view.Display.INVALID_DISPLAY import android.window.WindowContainerToken import androidx.core.util.forEach import androidx.core.util.keyIterator import androidx.core.util.valueIterator import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.util.KtProtoLog import java.io.PrintWriter import java.util.concurrent.Executor import java.util.function.Consumer /** Keeps track of task data related to desktop mode. */ class DesktopModeTaskRepository { /** Task data that is tracked per display */ private data class DisplayData( /** * Set of task ids that are marked as active in desktop mode. Active tasks in desktop mode * are freeform tasks that are visible or have been visible after desktop mode was * activated. Task gets removed from this list when it vanishes. Or when desktop mode is * turned off. */ val activeTasks: ArraySet = ArraySet(), val visibleTasks: ArraySet = ArraySet(), val minimizedTasks: ArraySet = ArraySet(), // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0). val freeformTasksInZOrder: ArrayList = ArrayList(), ) // Token of the current wallpaper activity, used to remove it when the last task is removed var wallpaperActivityToken: WindowContainerToken? = null private val activeTasksListeners = ArraySet() // Track visible tasks separately because a task may be part of the desktop but not visible. private val visibleTasksListeners = ArrayMap() // Track corner/caption regions of desktop tasks, used to determine gesture exclusion private val desktopExclusionRegions = SparseArray() // Track last bounds of task before toggled to stable bounds private val boundsBeforeMaximizeByTaskId = SparseArray() private var desktopGestureExclusionListener: Consumer? = null private var desktopGestureExclusionExecutor: Executor? = null private val displayData = object : SparseArray() { /** * Get the [DisplayData] associated with this [displayId] * * Creates a new instance if one does not exist */ fun getOrCreate(displayId: Int): DisplayData { if (!contains(displayId)) { put(displayId, DisplayData()) } return get(displayId) } } /** Add a [ActiveTasksListener] to be notified of updates to active tasks in the repository. */ fun addActiveTaskListener(activeTasksListener: ActiveTasksListener) { activeTasksListeners.add(activeTasksListener) } /** Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not. */ fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) { visibleTasksListeners[visibleTasksListener] = executor displayData.keyIterator().forEach { displayId -> val visibleTasksCount = getVisibleTaskCount(displayId) executor.execute { visibleTasksListener.onTasksVisibilityChanged(displayId, visibleTasksCount) } } } /** * Add a Consumer which will inform other classes of changes to exclusion regions for all * Desktop tasks. */ fun setExclusionRegionListener(regionListener: Consumer, executor: Executor) { desktopGestureExclusionListener = regionListener desktopGestureExclusionExecutor = executor executor.execute { desktopGestureExclusionListener?.accept(calculateDesktopExclusionRegion()) } } /** Create a new merged region representative of all exclusion regions in all desktop tasks. */ private fun calculateDesktopExclusionRegion(): Region { val desktopExclusionRegion = Region() desktopExclusionRegions.valueIterator().forEach { taskExclusionRegion -> desktopExclusionRegion.op(taskExclusionRegion, Region.Op.UNION) } return desktopExclusionRegion } /** Remove a previously registered [ActiveTasksListener] */ fun removeActiveTasksListener(activeTasksListener: ActiveTasksListener) { activeTasksListeners.remove(activeTasksListener) } /** Remove a previously registered [VisibleTasksListener] */ fun removeVisibleTasksListener(visibleTasksListener: VisibleTasksListener) { visibleTasksListeners.remove(visibleTasksListener) } /** * Mark a task with given [taskId] as active on given [displayId] * * @return `true` if the task was not active on given [displayId] */ fun addActiveTask(displayId: Int, taskId: Int): Boolean { // Check if task is active on another display, if so, remove it displayData.forEach { id, data -> if (id != displayId && data.activeTasks.remove(taskId)) { activeTasksListeners.onEach { it.onActiveTasksChanged(id) } } } val added = displayData.getOrCreate(displayId).activeTasks.add(taskId) if (added) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: add active task=%d displayId=%d", taskId, displayId ) activeTasksListeners.onEach { it.onActiveTasksChanged(displayId) } } return added } /** * Remove task with given [taskId] from active tasks. * * @return `true` if the task was active */ fun removeActiveTask(taskId: Int): Boolean { var result = false displayData.forEach { displayId, data -> if (data.activeTasks.remove(taskId)) { activeTasksListeners.onEach { it.onActiveTasksChanged(displayId) } result = true } } if (result) { KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: remove active task=%d", taskId) } return result } /** Check if a task with the given [taskId] was marked as an active task */ fun isActiveTask(taskId: Int): Boolean { return displayData.valueIterator().asSequence().any { data -> data.activeTasks.contains(taskId) } } /** Whether a task is visible. */ fun isVisibleTask(taskId: Int): Boolean { return displayData.valueIterator().asSequence().any { data -> data.visibleTasks.contains(taskId) } } /** Return whether the given Task is minimized. */ fun isMinimizedTask(taskId: Int): Boolean { return displayData.valueIterator().asSequence().any { data -> data.minimizedTasks.contains(taskId) } } /** Check if a task with the given [taskId] is the only active task on its display */ fun isOnlyActiveTask(taskId: Int): Boolean { return displayData.valueIterator().asSequence().any { data -> data.activeTasks.singleOrNull() == taskId } } /** Get a set of the active tasks for given [displayId] */ fun getActiveTasks(displayId: Int): ArraySet { return ArraySet(displayData[displayId]?.activeTasks) } /** * Returns whether Desktop Mode is currently showing any tasks, i.e. whether any Desktop Tasks * are visible. */ fun isDesktopModeShowing(displayId: Int): Boolean = getVisibleTaskCount(displayId) > 0 /** * Returns a list of Tasks IDs representing all active non-minimized Tasks on the given display, * ordered from front to back. */ fun getActiveNonMinimizedTasksOrderedFrontToBack(displayId: Int): List { val activeTasks = getActiveTasks(displayId) val allTasksInZOrder = getFreeformTasksInZOrder(displayId) return activeTasks // Don't show already minimized Tasks .filter { taskId -> !isMinimizedTask(taskId) } .sortedBy { taskId -> allTasksInZOrder.indexOf(taskId) } } /** Get a list of freeform tasks, ordered from top-bottom (top at index 0). */ fun getFreeformTasksInZOrder(displayId: Int): ArrayList = ArrayList(displayData[displayId]?.freeformTasksInZOrder ?: emptyList()) /** * Updates whether a freeform task with this id is visible or not and notifies listeners. * * If the task was visible on a different display with a different displayId, it is removed from * the set of visible tasks on that display. Listeners will be notified. */ fun updateVisibleFreeformTasks(displayId: Int, taskId: Int, visible: Boolean) { if (visible) { // Task is visible. Check if we need to remove it from any other display. val otherDisplays = displayData.keyIterator().asSequence().filter { it != displayId } for (otherDisplayId in otherDisplays) { if (displayData[otherDisplayId].visibleTasks.remove(taskId)) { notifyVisibleTaskListeners( otherDisplayId, displayData[otherDisplayId].visibleTasks.size ) } } } else if (displayId == INVALID_DISPLAY) { // Task has vanished. Check which display to remove the task from. displayData.forEach { displayId, data -> if (data.visibleTasks.remove(taskId)) { notifyVisibleTaskListeners(displayId, data.visibleTasks.size) } } return } val prevCount = getVisibleTaskCount(displayId) if (visible) { displayData.getOrCreate(displayId).visibleTasks.add(taskId) unminimizeTask(displayId, taskId) } else { displayData[displayId]?.visibleTasks?.remove(taskId) } val newCount = getVisibleTaskCount(displayId) // Check if count changed if (prevCount != newCount) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: update task visibility taskId=%d visible=%b displayId=%d", taskId, visible, displayId ) KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: visibleTaskCount has changed from %d to %d", prevCount, newCount ) notifyVisibleTaskListeners(displayId, newCount) } } private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) { visibleTasksListeners.forEach { (listener, executor) -> executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) } } } /** Get number of tasks that are marked as visible on given [displayId] */ fun getVisibleTaskCount(displayId: Int): Int { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: visibleTaskCount= %d", displayData[displayId]?.visibleTasks?.size ?: 0 ) return displayData[displayId]?.visibleTasks?.size ?: 0 } /** Add (or move if it already exists) the task to the top of the ordered list. */ // TODO(b/342417921): Identify if there is additional checks needed to move tasks for // multi-display scenarios. fun addOrMoveFreeformTaskToTop(displayId: Int, taskId: Int) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: add or move task to top: display=%d, taskId=%d", displayId, taskId ) displayData[displayId]?.freeformTasksInZOrder?.remove(taskId) displayData.getOrCreate(displayId).freeformTasksInZOrder.add(0, taskId) } /** Mark a Task as minimized. */ fun minimizeTask(displayId: Int, taskId: Int) { KtProtoLog.v( WM_SHELL_DESKTOP_MODE, "DesktopModeTaskRepository: minimize Task: display=%d, task=%d", displayId, taskId ) displayData.getOrCreate(displayId).minimizedTasks.add(taskId) } /** Mark a Task as non-minimized. */ fun unminimizeTask(displayId: Int, taskId: Int) { KtProtoLog.v( WM_SHELL_DESKTOP_MODE, "DesktopModeTaskRepository: unminimize Task: display=%d, task=%d", displayId, taskId ) displayData[displayId]?.minimizedTasks?.remove(taskId) } /** Remove the task from the ordered list. */ fun removeFreeformTask(displayId: Int, taskId: Int) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: remove freeform task from ordered list: display=%d, taskId=%d", displayId, taskId ) displayData[displayId]?.freeformTasksInZOrder?.remove(taskId) boundsBeforeMaximizeByTaskId.remove(taskId) KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: remaining freeform tasks: %s", displayData[displayId]?.freeformTasksInZOrder?.toDumpString() ?: "" ) } /** * Updates the active desktop gesture exclusion regions; if desktopExclusionRegions has been * accepted by desktopGestureExclusionListener, it will be updated in the appropriate classes. */ fun updateTaskExclusionRegions(taskId: Int, taskExclusionRegions: Region) { desktopExclusionRegions.put(taskId, taskExclusionRegions) desktopGestureExclusionExecutor?.execute { desktopGestureExclusionListener?.accept(calculateDesktopExclusionRegion()) } } /** * Removes the desktop gesture exclusion region for the specified task; if exclusionRegion has * been accepted by desktopGestureExclusionListener, it will be updated in the appropriate * classes. */ fun removeExclusionRegion(taskId: Int) { desktopExclusionRegions.delete(taskId) desktopGestureExclusionExecutor?.execute { desktopGestureExclusionListener?.accept(calculateDesktopExclusionRegion()) } } /** Removes and returns the bounds saved before maximizing the given task. */ fun removeBoundsBeforeMaximize(taskId: Int): Rect? { return boundsBeforeMaximizeByTaskId.removeReturnOld(taskId) } /** Saves the bounds of the given task before maximizing. */ fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) { boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds)) } internal fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("${prefix}DesktopModeTaskRepository") dumpDisplayData(pw, innerPrefix) pw.println("${innerPrefix}activeTasksListeners=${activeTasksListeners.size}") pw.println("${innerPrefix}visibleTasksListeners=${visibleTasksListeners.size}") } private fun dumpDisplayData(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " displayData.forEach { displayId, data -> pw.println("${prefix}Display $displayId:") pw.println("${innerPrefix}activeTasks=${data.activeTasks.toDumpString()}") pw.println("${innerPrefix}visibleTasks=${data.visibleTasks.toDumpString()}") pw.println( "${innerPrefix}freeformTasksInZOrder=${data.freeformTasksInZOrder.toDumpString()}" ) } } /** * Defines interface for classes that can listen to changes for active tasks in desktop mode. */ interface ActiveTasksListener { /** Called when the active tasks change in desktop mode. */ fun onActiveTasksChanged(displayId: Int) {} } /** * Defines interface for classes that can listen to changes for visible tasks in desktop mode. */ interface VisibleTasksListener { /** Called when the desktop changes the number of visible freeform tasks. */ fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {} } } private fun Iterable.toDumpString(): String { return joinToString(separator = ", ", prefix = "[", postfix = "]") }