• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.desktopmode
18 
19 import android.app.ActivityManager
20 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
21 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
22 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
23 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
24 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
25 import android.app.WindowConfiguration.WindowingMode
26 import android.content.Context
27 import android.os.IBinder
28 import android.os.SystemProperties
29 import android.view.SurfaceControl
30 import android.view.WindowManager.TRANSIT_CHANGE
31 import android.view.WindowManager.TRANSIT_NONE
32 import android.view.WindowManager.TRANSIT_OPEN
33 import android.view.WindowManager.TRANSIT_TO_FRONT
34 import android.window.TransitionInfo
35 import android.window.TransitionRequestInfo
36 import android.window.WindowContainerToken
37 import android.window.WindowContainerTransaction
38 import androidx.annotation.BinderThread
39 import com.android.internal.protolog.common.ProtoLog
40 import com.android.wm.shell.ShellTaskOrganizer
41 import com.android.wm.shell.common.ExecutorUtils
42 import com.android.wm.shell.common.ExternalInterfaceBinder
43 import com.android.wm.shell.common.RemoteCallable
44 import com.android.wm.shell.common.ShellExecutor
45 import com.android.wm.shell.common.annotations.ExternalThread
46 import com.android.wm.shell.common.annotations.ShellMainThread
47 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener
48 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
49 import com.android.wm.shell.sysui.ShellController
50 import com.android.wm.shell.sysui.ShellInit
51 import com.android.wm.shell.sysui.ShellSharedConstants
52 import com.android.wm.shell.transition.Transitions
53 import java.util.concurrent.Executor
54 import java.util.function.Consumer
55 
56 /** Handles moving tasks in and out of desktop */
57 class DesktopTasksController(
58     private val context: Context,
59     shellInit: ShellInit,
60     private val shellController: ShellController,
61     private val shellTaskOrganizer: ShellTaskOrganizer,
62     private val transitions: Transitions,
63     private val desktopModeTaskRepository: DesktopModeTaskRepository,
64     @ShellMainThread private val mainExecutor: ShellExecutor
65 ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler {
66 
67     private val desktopMode: DesktopModeImpl
68 
69     init {
70         desktopMode = DesktopModeImpl()
71         if (DesktopModeStatus.isProto2Enabled()) {
72             shellInit.addInitCallback({ onInit() }, this)
73         }
74     }
75 
76     private fun onInit() {
77         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController")
78         shellController.addExternalInterface(
79             ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE,
80             { createExternalInterface() },
81             this
82         )
83         transitions.addHandler(this)
84     }
85 
86     /** Show all tasks, that are part of the desktop, on top of launcher */
87     fun showDesktopApps() {
88         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "showDesktopApps")
89         val wct = WindowContainerTransaction()
90         bringDesktopAppsToFront(wct)
91 
92         // Execute transaction if there are pending operations
93         if (!wct.isEmpty) {
94             if (Transitions.ENABLE_SHELL_TRANSITIONS) {
95                 // TODO(b/268662477): add animation for the transition
96                 transitions.startTransition(TRANSIT_NONE, wct, null /* handler */)
97             } else {
98                 shellTaskOrganizer.applyTransaction(wct)
99             }
100         }
101     }
102 
103     /** Get number of tasks that are marked as visible */
104     fun getVisibleTaskCount(): Int {
105         return desktopModeTaskRepository.getVisibleTaskCount()
106     }
107 
108     /** Move a task with given `taskId` to desktop */
109     fun moveToDesktop(taskId: Int) {
110         shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToDesktop(task) }
111     }
112 
113     /** Move a task to desktop */
114     fun moveToDesktop(task: ActivityManager.RunningTaskInfo) {
115         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToDesktop: %d", task.taskId)
116 
117         val wct = WindowContainerTransaction()
118         // Bring other apps to front first
119         bringDesktopAppsToFront(wct)
120         addMoveToDesktopChanges(wct, task.token)
121         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
122             transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
123         } else {
124             shellTaskOrganizer.applyTransaction(wct)
125         }
126     }
127 
128     /** Move a task with given `taskId` to fullscreen */
129     fun moveToFullscreen(taskId: Int) {
130         shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToFullscreen(task) }
131     }
132 
133     /** Move a task to fullscreen */
134     fun moveToFullscreen(task: ActivityManager.RunningTaskInfo) {
135         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToFullscreen: %d", task.taskId)
136 
137         val wct = WindowContainerTransaction()
138         addMoveToFullscreenChanges(wct, task.token)
139         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
140             transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
141         } else {
142             shellTaskOrganizer.applyTransaction(wct)
143         }
144     }
145 
146     /**
147      * Get windowing move for a given `taskId`
148      *
149      * @return [WindowingMode] for the task or [WINDOWING_MODE_UNDEFINED] if task is not found
150      */
151     @WindowingMode
152     fun getTaskWindowingMode(taskId: Int): Int {
153         return shellTaskOrganizer.getRunningTaskInfo(taskId)?.windowingMode
154             ?: WINDOWING_MODE_UNDEFINED
155     }
156 
157     private fun bringDesktopAppsToFront(wct: WindowContainerTransaction) {
158         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront")
159         val activeTasks = desktopModeTaskRepository.getActiveTasks()
160 
161         // First move home to front and then other tasks on top of it
162         moveHomeTaskToFront(wct)
163 
164         val allTasksInZOrder = desktopModeTaskRepository.getFreeformTasksInZOrder()
165         activeTasks
166             // Sort descending as the top task is at index 0. It should be ordered to top last
167             .sortedByDescending { taskId -> allTasksInZOrder.indexOf(taskId) }
168             .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) }
169             .forEach { task -> wct.reorder(task.token, true /* onTop */) }
170     }
171 
172     private fun moveHomeTaskToFront(wct: WindowContainerTransaction) {
173         shellTaskOrganizer
174             .getRunningTasks(context.displayId)
175             .firstOrNull { task -> task.activityType == ACTIVITY_TYPE_HOME }
176             ?.let { homeTask -> wct.reorder(homeTask.getToken(), true /* onTop */) }
177     }
178 
179     override fun getContext(): Context {
180         return context
181     }
182 
183     override fun getRemoteCallExecutor(): ShellExecutor {
184         return mainExecutor
185     }
186 
187     override fun startAnimation(
188         transition: IBinder,
189         info: TransitionInfo,
190         startTransaction: SurfaceControl.Transaction,
191         finishTransaction: SurfaceControl.Transaction,
192         finishCallback: Transitions.TransitionFinishCallback
193     ): Boolean {
194         // This handler should never be the sole handler, so should not animate anything.
195         return false
196     }
197 
198     override fun handleRequest(
199         transition: IBinder,
200         request: TransitionRequestInfo
201     ): WindowContainerTransaction? {
202         // Check if we should skip handling this transition
203         val task: ActivityManager.RunningTaskInfo? = request.triggerTask
204         val shouldHandleRequest =
205             when {
206                 // Only handle open or to front transitions
207                 request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> false
208                 // Only handle when it is a task transition
209                 task == null -> false
210                 // Only handle standard type tasks
211                 task.activityType != ACTIVITY_TYPE_STANDARD -> false
212                 // Only handle fullscreen or freeform tasks
213                 task.windowingMode != WINDOWING_MODE_FULLSCREEN &&
214                     task.windowingMode != WINDOWING_MODE_FREEFORM -> false
215                 // Otherwise process it
216                 else -> true
217             }
218 
219         if (!shouldHandleRequest) {
220             return null
221         }
222 
223         val activeTasks = desktopModeTaskRepository.getActiveTasks()
224 
225         // Check if we should switch a fullscreen task to freeform
226         if (task?.windowingMode == WINDOWING_MODE_FULLSCREEN) {
227             // If there are any visible desktop tasks, switch the task to freeform
228             if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) {
229                 ProtoLog.d(
230                     WM_SHELL_DESKTOP_MODE,
231                     "DesktopTasksController#handleRequest: switch fullscreen task to freeform," +
232                         " taskId=%d",
233                     task.taskId
234                 )
235                 return WindowContainerTransaction().also { wct ->
236                     addMoveToDesktopChanges(wct, task.token)
237                 }
238             }
239         }
240 
241         // CHeck if we should switch a freeform task to fullscreen
242         if (task?.windowingMode == WINDOWING_MODE_FREEFORM) {
243             // If no visible desktop tasks, switch this task to freeform as the transition came
244             // outside of this controller
245             if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) {
246                 ProtoLog.d(
247                     WM_SHELL_DESKTOP_MODE,
248                     "DesktopTasksController#handleRequest: switch freeform task to fullscreen," +
249                         " taskId=%d",
250                     task.taskId
251                 )
252                 return WindowContainerTransaction().also { wct ->
253                     addMoveToFullscreenChanges(wct, task.token)
254                 }
255             }
256         }
257         return null
258     }
259 
260     private fun addMoveToDesktopChanges(
261         wct: WindowContainerTransaction,
262         token: WindowContainerToken
263     ) {
264         wct.setWindowingMode(token, WINDOWING_MODE_FREEFORM)
265         wct.reorder(token, true /* onTop */)
266         if (isDesktopDensityOverrideSet()) {
267             wct.setDensityDpi(token, getDesktopDensityDpi())
268         }
269     }
270 
271     private fun addMoveToFullscreenChanges(
272         wct: WindowContainerTransaction,
273         token: WindowContainerToken
274     ) {
275         wct.setWindowingMode(token, WINDOWING_MODE_FULLSCREEN)
276         wct.setBounds(token, null)
277         if (isDesktopDensityOverrideSet()) {
278             wct.setDensityDpi(token, getFullscreenDensityDpi())
279         }
280     }
281 
282     private fun getFullscreenDensityDpi(): Int {
283         return context.resources.displayMetrics.densityDpi
284     }
285 
286     private fun getDesktopDensityDpi(): Int {
287         return DESKTOP_DENSITY_OVERRIDE
288     }
289 
290     /** Creates a new instance of the external interface to pass to another process. */
291     private fun createExternalInterface(): ExternalInterfaceBinder {
292         return IDesktopModeImpl(this)
293     }
294 
295     /** Get connection interface between sysui and shell */
296     fun asDesktopMode(): DesktopMode {
297         return desktopMode
298     }
299 
300     /**
301      * Adds a listener to find out about changes in the visibility of freeform tasks.
302      *
303      * @param listener the listener to add.
304      * @param callbackExecutor the executor to call the listener on.
305      */
306     fun addListener(listener: VisibleTasksListener, callbackExecutor: Executor) {
307         desktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor)
308     }
309 
310     /** The interface for calls from outside the shell, within the host process. */
311     @ExternalThread
312     private inner class DesktopModeImpl : DesktopMode {
313         override fun addListener(listener: VisibleTasksListener, callbackExecutor: Executor) {
314             mainExecutor.execute {
315                 this@DesktopTasksController.addListener(listener, callbackExecutor)
316             }
317         }
318     }
319 
320     /** The interface for calls from outside the host process. */
321     @BinderThread
322     private class IDesktopModeImpl(private var controller: DesktopTasksController?) :
323         IDesktopMode.Stub(), ExternalInterfaceBinder {
324         /** Invalidates this instance, preventing future calls from updating the controller. */
325         override fun invalidate() {
326             controller = null
327         }
328 
329         override fun showDesktopApps() {
330             ExecutorUtils.executeRemoteCallWithTaskPermission(
331                 controller,
332                 "showDesktopApps",
333                 Consumer(DesktopTasksController::showDesktopApps)
334             )
335         }
336 
337         override fun getVisibleTaskCount(): Int {
338             val result = IntArray(1)
339             ExecutorUtils.executeRemoteCallWithTaskPermission(
340                 controller,
341                 "getVisibleTaskCount",
342                 { controller -> result[0] = controller.getVisibleTaskCount() },
343                 true /* blocking */
344             )
345             return result[0]
346         }
347     }
348 
349     companion object {
350         private val DESKTOP_DENSITY_OVERRIDE =
351             SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 0)
352         private val DESKTOP_DENSITY_ALLOWED_RANGE = (100..1000)
353 
354         /**
355          * Check if desktop density override is enabled
356          */
357         @JvmStatic
358         fun isDesktopDensityOverrideSet(): Boolean {
359             return DESKTOP_DENSITY_OVERRIDE in DESKTOP_DENSITY_ALLOWED_RANGE
360         }
361     }
362 }
363