• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.systemui.screenshot
18 
19 import android.R
20 import android.annotation.MainThread
21 import android.content.Context
22 import android.graphics.PixelFormat
23 import android.os.IBinder
24 import android.util.Log
25 import android.view.Display
26 import android.view.View
27 import android.view.ViewGroup
28 import android.view.ViewRootImpl
29 import android.view.ViewTreeObserver.OnWindowAttachListener
30 import android.view.Window
31 import android.view.WindowInsets
32 import android.view.WindowManager
33 import android.window.WindowContext
34 import com.android.internal.policy.PhoneWindow
35 import dagger.assisted.Assisted
36 import dagger.assisted.AssistedFactory
37 import dagger.assisted.AssistedInject
38 
39 /** Creates and manages the window in which the screenshot UI is displayed. */
40 class ScreenshotWindow
41 @AssistedInject
42 constructor(
43     private val windowManager: WindowManager,
44     private val context: Context,
45     @Assisted private val display: Display,
46 ) {
47 
48     val window: PhoneWindow =
49         PhoneWindow(
50             context
51                 .createDisplayContext(display)
52                 .createWindowContext(WindowManager.LayoutParams.TYPE_SCREENSHOT, null)
53         )
54     private val params =
55         WindowManager.LayoutParams(
56                 ViewGroup.LayoutParams.MATCH_PARENT,
57                 ViewGroup.LayoutParams.MATCH_PARENT,
58                 0, /* xpos */
59                 0, /* ypos */
60                 WindowManager.LayoutParams.TYPE_SCREENSHOT,
61                 WindowManager.LayoutParams.FLAG_FULLSCREEN or
62                     WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
63                     WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
64                     WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
65                     WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
66                     WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
67                 PixelFormat.TRANSLUCENT
68             )
69             .apply {
70                 layoutInDisplayCutoutMode =
71                     WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
72                 setFitInsetsTypes(0)
73                 // This is needed to let touches pass through outside the touchable areas
74                 privateFlags =
75                     privateFlags or WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
76                 title = "ScreenshotUI"
77             }
78     private var attachRequested: Boolean = false
79     private var detachRequested: Boolean = false
80 
81     init {
82         window.requestFeature(Window.FEATURE_NO_TITLE)
83         window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
84         window.setBackgroundDrawableResource(R.color.transparent)
85         window.setWindowManager(windowManager, null, null)
86     }
87 
88     @MainThread
89     fun attachWindow() {
90         val decorView: View = window.getDecorView()
91         if (decorView.isAttachedToWindow || attachRequested) {
92             return
93         }
94         if (LogConfig.DEBUG_WINDOW) {
95             Log.d(TAG, "attachWindow")
96         }
97         attachRequested = true
98         windowManager.addView(decorView, params)
99 
100         decorView.requestApplyInsets()
101         decorView.requireViewById<ViewGroup>(R.id.content).apply {
102             clipChildren = false
103             clipToPadding = false
104             // ignore system bar insets for the purpose of window layout
105             setOnApplyWindowInsetsListener { _, _ -> WindowInsets.CONSUMED }
106         }
107     }
108 
109     fun whenWindowAttached(action: Runnable) {
110         val decorView: View = window.getDecorView()
111         if (decorView.isAttachedToWindow) {
112             action.run()
113         } else {
114             decorView
115                 .getViewTreeObserver()
116                 .addOnWindowAttachListener(
117                     object : OnWindowAttachListener {
118                         override fun onWindowAttached() {
119                             attachRequested = false
120                             decorView.getViewTreeObserver().removeOnWindowAttachListener(this)
121                             action.run()
122                         }
123 
124                         override fun onWindowDetached() {}
125                     }
126                 )
127         }
128     }
129 
130     fun removeWindow() {
131         val decorView: View? = window.peekDecorView()
132         if (decorView != null && decorView.isAttachedToWindow) {
133             if (LogConfig.DEBUG_WINDOW) {
134                 Log.d(TAG, "Removing screenshot window")
135             }
136             windowManager.removeViewImmediate(decorView)
137             detachRequested = false
138         }
139         if (attachRequested && !detachRequested) {
140             detachRequested = true
141             whenWindowAttached { removeWindow() }
142         }
143     }
144 
145     /**
146      * Updates the window focusability. If the window is already showing, then it updates the window
147      * immediately, otherwise the layout params will be applied when the window is next shown.
148      */
149     fun setFocusable(focusable: Boolean) {
150         if (LogConfig.DEBUG_WINDOW) {
151             Log.d(TAG, "setWindowFocusable: $focusable")
152         }
153         val flags: Int = params.flags
154         if (focusable) {
155             params.flags = params.flags and WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv()
156         } else {
157             params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
158         }
159         if (params.flags == flags) {
160             if (LogConfig.DEBUG_WINDOW) {
161                 Log.d(TAG, "setWindowFocusable: skipping, already $focusable")
162             }
163             return
164         }
165         window.peekDecorView()?.also {
166             if (it.isAttachedToWindow) {
167                 windowManager.updateViewLayout(it, params)
168             }
169         }
170     }
171 
172     fun getContext(): WindowContext = window.context as WindowContext
173 
174     fun getWindowToken(): IBinder = window.decorView.windowToken
175 
176     fun getWindowInsets(): WindowInsets = windowManager.currentWindowMetrics.windowInsets
177 
178     fun setContentView(view: View) {
179         window.setContentView(view)
180     }
181 
182     fun setActivityConfigCallback(callback: ViewRootImpl.ActivityConfigCallback) {
183         window.peekDecorView().viewRootImpl.setActivityConfigCallback(callback)
184     }
185 
186     @AssistedFactory
187     interface Factory {
188         fun create(display: Display): ScreenshotWindow
189     }
190 
191     companion object {
192         private const val TAG = "ScreenshotWindow"
193     }
194 }
195