1 /*
<lambda>null2  * Copyright (C) 2023 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.util
18 
19 import android.app.Dialog
20 import android.view.View
21 import android.view.ViewGroup
22 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
23 import android.widget.FrameLayout
24 import android.window.OnBackInvokedDispatcher
25 import com.android.systemui.animation.back.BackAnimationSpec
26 import com.android.systemui.animation.back.BackTransformation
27 import com.android.systemui.animation.back.applyTo
28 import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi
29 import com.android.systemui.animation.back.onBackAnimationCallbackFrom
30 import com.android.systemui.animation.back.registerOnBackInvokedCallbackOnViewAttached
31 import com.android.systemui.animation.view.LaunchableFrameLayout
32 
33 /**
34  * Register on the Dialog's [OnBackInvokedDispatcher] an animation using the [BackAnimationSpec].
35  * The [BackTransformation] will be applied on the [targetView].
36  */
37 @JvmOverloads
38 fun Dialog.registerAnimationOnBackInvoked(
39     targetView: View,
40     backAnimationSpec: BackAnimationSpec =
41         BackAnimationSpec.floatingSystemSurfacesForSysUi(
42             displayMetricsProvider = { targetView.resources.displayMetrics },
43         ),
44 ) {
45     targetView.registerOnBackInvokedCallbackOnViewAttached(
46         onBackInvokedDispatcher = onBackInvokedDispatcher,
47         onBackAnimationCallback =
48             onBackAnimationCallbackFrom(
49                 backAnimationSpec = backAnimationSpec,
50                 displayMetrics = targetView.resources.displayMetrics,
backTransformationnull51                 onBackProgressed = { backTransformation -> backTransformation.applyTo(targetView) },
<lambda>null52                 onBackInvoked = { dismiss() },
53             ),
54     )
55 }
56 
57 /**
58  * Make the dialog window (and therefore its DecorView) fullscreen to make it possible to animate
59  * outside its bounds. No-op if the dialog is already fullscreen.
60  *
61  * <p>Returns null if the dialog is already fullscreen. Otherwise, returns a pair containing a view
62  * and a layout listener. The new view matches the original dialog DecorView in size, position, and
63  * background. This new view will be a child of the modified, transparent, fullscreen DecorView. The
64  * layout listener is listening to changes to the modified DecorView. It is the responsibility of
65  * the caller to deregister the listener when the dialog is dismissed.
66  */
maybeForceFullscreennull67 fun Dialog.maybeForceFullscreen(): Pair<LaunchableFrameLayout, View.OnLayoutChangeListener>? {
68     // Create the dialog so that its onCreate() method is called, which usually sets the dialog
69     // content.
70     create()
71 
72     val window = window!!
73     val decorView = window.decorView as ViewGroup
74 
75     val isWindowFullscreen =
76         window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT
77     if (isWindowFullscreen) {
78         return null
79     }
80 
81     // We will make the dialog window (and therefore its DecorView) fullscreen to make it possible
82     // to animate outside its bounds.
83     //
84     // Before that, we add a new View as a child of the DecorView with the same size and gravity as
85     // that DecorView, then we add all original children of the DecorView to that new View. Finally
86     // we remove the background of the DecorView and add it to the new View, then we make the
87     // DecorView fullscreen. This new View now acts as a fake (non fullscreen) window.
88     //
89     // On top of that, we also add a fullscreen transparent background between the DecorView and the
90     // view that we added so that we can dismiss the dialog when this view is clicked. This is
91     // necessary because DecorView overrides onTouchEvent and therefore we can't set the click
92     // listener directly on the (now fullscreen) DecorView.
93     val fullscreenTransparentBackground = FrameLayout(context)
94     decorView.addView(
95         fullscreenTransparentBackground,
96         0 /* index */,
97         FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
98     )
99 
100     val dialogContentWithBackground = LaunchableFrameLayout(context)
101     dialogContentWithBackground.background = decorView.background
102 
103     // Make the window background transparent. Note that setting the window (or DecorView)
104     // background drawable to null leads to issues with background color (not being transparent) or
105     // with insets that are not refreshed. Therefore we need to set it to something not null, hence
106     // we are using android.R.color.transparent here.
107     window.setBackgroundDrawableResource(android.R.color.transparent)
108 
109     // Close the dialog when clicking outside of it.
110     fullscreenTransparentBackground.setOnClickListener { dismiss() }
111     dialogContentWithBackground.isClickable = true
112 
113     // Make sure the transparent and dialog backgrounds are not focusable by accessibility
114     // features.
115     fullscreenTransparentBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
116     dialogContentWithBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
117 
118     fullscreenTransparentBackground.addView(
119         dialogContentWithBackground,
120         FrameLayout.LayoutParams(
121             window.attributes.width,
122             window.attributes.height,
123             window.attributes.gravity
124         )
125     )
126 
127     // Move all original children of the DecorView to the new View we just added.
128     for (i in 1 until decorView.childCount) {
129         val view = decorView.getChildAt(1)
130         decorView.removeViewAt(1)
131         dialogContentWithBackground.addView(view)
132     }
133 
134     // Make the window fullscreen and add a layout listener to ensure it stays fullscreen.
135     window.setLayout(MATCH_PARENT, MATCH_PARENT)
136     val decorViewLayoutListener =
137         View.OnLayoutChangeListener {
138             v,
139             left,
140             top,
141             right,
142             bottom,
143             oldLeft,
144             oldTop,
145             oldRight,
146             oldBottom ->
147             if (
148                 window.attributes.width != MATCH_PARENT || window.attributes.height != MATCH_PARENT
149             ) {
150                 // The dialog size changed, copy its size to dialogContentWithBackground and make
151                 // the dialog window full screen again.
152                 val layoutParams = dialogContentWithBackground.layoutParams
153                 layoutParams.width = window.attributes.width
154                 layoutParams.height = window.attributes.height
155                 dialogContentWithBackground.layoutParams = layoutParams
156                 window.setLayout(MATCH_PARENT, MATCH_PARENT)
157             }
158         }
159     decorView.addOnLayoutChangeListener(decorViewLayoutListener)
160 
161     return dialogContentWithBackground to decorViewLayoutListener
162 }
163