/* * 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.launcher3.taskbar import android.content.Context import android.graphics.Bitmap import android.view.MotionEvent import android.view.View import com.android.launcher3.AbstractFloatingView import com.android.launcher3.R import com.android.launcher3.Utilities import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.popup.SystemShortcut import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext import com.android.launcher3.util.TouchController import com.android.launcher3.views.ActivityContext import com.android.quickstep.RecentsModel import com.android.quickstep.SystemUiProxy import com.android.quickstep.util.DesktopTask import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.ThumbnailData import com.android.wm.shell.shared.desktopmode.DesktopTaskToFrontReason import com.android.wm.shell.shared.multiinstance.ManageWindowsViewContainer /** * A single menu item shortcut to execute displaying open instances of an app. Default interaction * for [onClick] is to open the menu in a floating window. Touching one of the displayed tasks * launches it. */ class ManageWindowsTaskbarShortcut( private val target: T, private val itemInfo: ItemInfo?, private val originalView: View, private val controllers: TaskbarControllers, ) : SystemShortcut( R.drawable.desktop_mode_ic_taskbar_menu_manage_windows, R.string.manage_windows_option_taskbar, target, itemInfo, originalView, ) where T : Context?, T : ActivityContext? { private lateinit var taskbarShortcutAllWindowsView: TaskbarShortcutManageWindowsView private val recentsModel = RecentsModel.INSTANCE[controllers.taskbarActivityContext] override fun onClick(v: View?) { val targetPackage = itemInfo?.getTargetPackage() val targetUserId = itemInfo?.user?.identifier val isTargetPackageTask: (Task) -> Boolean = { task -> task.key?.packageName == targetPackage && task.key.userId == targetUserId } recentsModel.getTasks { tasks -> val desktopTask = tasks.filterIsInstance().firstOrNull() val packageDesktopTasks = (desktopTask?.tasks ?: emptyList()).filter(isTargetPackageTask) val nonDesktopPackageTasks = tasks.flatMap { it.tasks }.filter { isTargetPackageTask(it) } // Add tasks from the fetched tasks, deduplicating by task ID val packageTasks = (packageDesktopTasks + nonDesktopPackageTasks).distinctBy { it.key.id } // Since fetching thumbnails is asynchronous, use `awaitedTaskIds` to gate until the // tasks are ready to display val awaitedTaskIds = packageTasks.map { it.key.id }.toMutableSet() createAndShowTaskShortcutView(packageTasks, awaitedTaskIds) } } /** * Processes a list of tasks to generate thumbnails and create a taskbar shortcut view. * * Iterates through the tasks, retrieves thumbnails, and adds them to a list. When all * thumbnails are processed, it creates a [TaskbarShortcutManageWindowsView] with the collected * thumbnails and positions it appropriately. */ private fun createAndShowTaskShortcutView(tasks: List, pendingTaskIds: MutableSet) { val taskList = arrayListOf>() tasks.forEach { task -> recentsModel.thumbnailCache.getThumbnailInBackground(task) { thumbnailData: ThumbnailData -> pendingTaskIds.remove(task.key.id) // Add the current pair of task id and ThumbnailData to the list of all tasks if (thumbnailData.thumbnail != null) { taskList.add(task.key.id to thumbnailData.thumbnail) } // If the set is empty, all thumbnails have been fetched if (pendingTaskIds.isEmpty() && taskList.isNotEmpty()) { createAndPositionTaskbarShortcut(taskList) } } } } /** * Creates and positions the [TaskbarShortcutManageWindowsView] with the provided thumbnails. */ private fun createAndPositionTaskbarShortcut(taskList: ArrayList>) { val onIconClickListener = ({ taskId: Int? -> taskbarShortcutAllWindowsView.animateClose() if (taskId != null) { SystemUiProxy.INSTANCE.get(target) .showDesktopApp( taskId, /* transition= */ null, DesktopTaskToFrontReason.TASKBAR_MANAGE_WINDOW, ) } }) val onOutsideClickListener = { taskbarShortcutAllWindowsView.animateClose() } taskbarShortcutAllWindowsView = TaskbarShortcutManageWindowsView( originalView, controllers.taskbarOverlayController.requestWindow(), taskList, onIconClickListener, onOutsideClickListener, controllers, ) // If the view is removed from elsewhere, reset the state to allow the taskbar to auto-stash taskbarShortcutAllWindowsView.menuView.rootView.addOnAttachStateChangeListener( object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { return } override fun onViewDetachedFromWindow(v: View) { controllers.taskbarAutohideSuspendController.updateFlag( FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN, false, ) controllers.taskbarPopupController.cleanUpMultiInstanceMenuReference() } } ) } /** Closes the multi-instance menu if it has been initialized. */ fun closeMultiInstanceMenu() { if (::taskbarShortcutAllWindowsView.isInitialized) { taskbarShortcutAllWindowsView.animateClose() } } /** * A view container for displaying the window of open instances of an app * * Handles showing the window snapshots, adding the carousel to the overlay, and closing it. * Also acts as a touch controller to intercept touch events outside the carousel to close it. */ class TaskbarShortcutManageWindowsView( private val originalView: View, private val taskbarOverlayContext: TaskbarOverlayContext, snapshotList: ArrayList>, onIconClickListener: (Int) -> Unit, onOutsideClickListener: () -> Unit, private val controllers: TaskbarControllers, ) : ManageWindowsViewContainer( originalView.context, originalView.context.getColor(R.color.materialColorSurfaceBright), ), TouchController { private val taskbarActivityContext = controllers.taskbarActivityContext init { createAndShowMenuView(snapshotList, onIconClickListener, onOutsideClickListener) taskbarOverlayContext.dragLayer.addTouchController(this) animateOpen() } /** Adds the carousel menu to the taskbar overlay drag layer */ override fun addToContainer(menuView: ManageWindowsView) { positionCarouselMenu() controllers.taskbarAutohideSuspendController.updateFlag( FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN, true, ) AbstractFloatingView.closeAllOpenViewsExcept( taskbarActivityContext, AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY, ) menuView.rootView.minimumHeight = menuView.menuHeight menuView.rootView.minimumWidth = menuView.menuWidth taskbarOverlayContext.dragLayer?.addView(menuView.rootView) menuView.rootView.requestFocus() } /** * Positions the carousel menu relative to the taskbar and the calling app's icon. * * Calculates the Y position to place the carousel above the taskbar, and the X position to * align with the calling app while ensuring it doesn't go beyond the screen edge. */ private fun positionCarouselMenu() { val deviceProfile = taskbarActivityContext.deviceProfile val margin = context.resources.getDimension( R.dimen.taskbar_multi_instance_menu_min_padding_from_screen_edge ) // Calculate the Y position to place the carousel above the taskbar menuView.rootView.y = deviceProfile.availableHeightPx - menuView.menuHeight - controllers.taskbarStashController.touchableHeight - margin // Calculate the X position to align with the calling app, // but avoid clashing with the screen edge menuView.rootView.translationX = if (Utilities.isRtl(context.resources)) { -(deviceProfile.availableWidthPx - menuView.menuWidth) / 2f } else { val maxX = deviceProfile.availableWidthPx - menuView.menuWidth - margin minOf(originalView.x, maxX) } } /** Closes the carousel menu and removes it from the taskbar overlay drag layer */ override fun removeFromContainer() { controllers.taskbarAutohideSuspendController.updateFlag( FLAG_AUTOHIDE_SUSPEND_MULTI_INSTANCE_MENU_OPEN, false, ) taskbarOverlayContext.dragLayer?.removeView(menuView.rootView) taskbarOverlayContext.dragLayer.removeTouchController(this) controllers.taskbarPopupController.cleanUpMultiInstanceMenuReference() } /** TouchController implementations for closing the carousel when touched outside */ override fun onControllerTouchEvent(ev: MotionEvent?): Boolean { return false } override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean { ev?.let { if ( it.action == MotionEvent.ACTION_DOWN && !taskbarOverlayContext.dragLayer.isEventOverView(menuView.rootView, it) ) { animateClose() } } return false } } }