1 /*
<lambda>null2  * Copyright 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 androidx.compose.ui.test
18 
19 import android.content.res.Configuration
20 import android.util.DisplayMetrics
21 import android.view.ContextThemeWrapper
22 import android.view.View
23 import android.view.WindowInsets
24 import androidx.annotation.RequiresApi
25 import androidx.compose.runtime.Composable
26 import androidx.compose.runtime.CompositionLocalProvider
27 import androidx.compose.runtime.getValue
28 import androidx.compose.runtime.rememberUpdatedState
29 import androidx.compose.ui.platform.AbstractComposeView
30 import androidx.compose.ui.platform.LocalConfiguration
31 import androidx.compose.ui.platform.LocalContext
32 import androidx.compose.ui.platform.LocalDensity
33 import androidx.compose.ui.platform.LocalFontFamilyResolver
34 import androidx.compose.ui.platform.LocalLayoutDirection
35 import androidx.compose.ui.text.font.createFontFamilyResolver
36 import androidx.compose.ui.text.intl.Locale
37 import androidx.compose.ui.text.intl.LocaleList
38 import androidx.compose.ui.unit.Density
39 import androidx.compose.ui.unit.DpSize
40 import androidx.compose.ui.unit.LayoutDirection
41 import androidx.compose.ui.util.fastJoinToString
42 import androidx.compose.ui.viewinterop.AndroidView
43 import androidx.core.os.ConfigurationCompat
44 import androidx.core.os.LocaleListCompat
45 import androidx.core.view.WindowInsetsCompat
46 import androidx.core.view.children
47 import kotlin.math.floor
48 
49 actual fun DeviceConfigurationOverride.Companion.ForcedSize(
50     size: DpSize
51 ): DeviceConfigurationOverride = DeviceConfigurationOverride { contentUnderTest ->
52     // First override the density. Doing this first allows using the resulting density in the
53     // overridden configuration.
54     DensityForcedSize(size) {
55         // Second, override the configuration, with the current configuration modified by the
56         // resulting density
57         OverriddenConfiguration(
58             configuration =
59                 Configuration().apply {
60                     // Initialize from the current configuration
61                     updateFrom(LocalConfiguration.current)
62 
63                     // Override densityDpi
64                     densityDpi =
65                         floor(LocalDensity.current.density * DisplayMetrics.DENSITY_DEFAULT).toInt()
66                 },
67             content = contentUnderTest
68         )
69     }
70 }
71 
FontScalenull72 actual fun DeviceConfigurationOverride.Companion.FontScale(
73     fontScale: Float
74 ): DeviceConfigurationOverride = DeviceConfigurationOverride { contentUnderTest ->
75     OverriddenConfiguration(
76         configuration =
77             Configuration().apply {
78                 // Initialize from the current configuration
79                 updateFrom(LocalConfiguration.current)
80 
81                 // Override font scale
82                 this.fontScale = fontScale
83             },
84         content = contentUnderTest
85     )
86 }
87 
LayoutDirectionnull88 actual fun DeviceConfigurationOverride.Companion.LayoutDirection(
89     layoutDirection: LayoutDirection
90 ): DeviceConfigurationOverride = DeviceConfigurationOverride { contentUnderTest ->
91     OverriddenConfiguration(
92         configuration =
93             Configuration().apply {
94                 // Initialize from the current configuration
95                 updateFrom(LocalConfiguration.current)
96 
97                 // Override screen layout for layout direction
98                 screenLayout =
99                     screenLayout and
100                         Configuration.SCREENLAYOUT_LAYOUTDIR_MASK.inv() or
101                         when (layoutDirection) {
102                             LayoutDirection.Ltr -> Configuration.SCREENLAYOUT_LAYOUTDIR_LTR
103                             LayoutDirection.Rtl -> Configuration.SCREENLAYOUT_LAYOUTDIR_RTL
104                         }
105             },
106         content = contentUnderTest
107     )
108 }
109 
110 /**
111  * A [DeviceConfigurationOverride] that overrides the locales for the contained content.
112  *
113  * This will change resource resolution for the content under test, and also override the layout
114  * direction as specified by the locales.
115  *
116  * @sample androidx.compose.ui.test.samples.DeviceConfigurationOverrideLocalesSample
117  */
Localesnull118 fun DeviceConfigurationOverride.Companion.Locales(
119     locales: LocaleList,
120 ): DeviceConfigurationOverride = DeviceConfigurationOverride { contentUnderTest ->
121     OverriddenConfiguration(
122         configuration =
123             Configuration().apply {
124                 // Initialize from the current configuration
125                 updateFrom(LocalConfiguration.current)
126 
127                 // Update the locale list
128                 ConfigurationCompat.setLocales(
129                     this,
130                     LocaleListCompat.forLanguageTags(
131                         locales.localeList.fastJoinToString(",", transform = Locale::toLanguageTag)
132                     )
133                 )
134             },
135         content = contentUnderTest
136     )
137 }
138 
139 /**
140  * A [DeviceConfigurationOverride] that overrides the dark mode or light mode theme for the
141  * contained content. Inside the content under test, `isSystemInDarkTheme()` will return
142  * [isDarkMode].
143  *
144  * @sample androidx.compose.ui.test.samples.DeviceConfigurationOverrideDarkModeSample
145  */
DarkModenull146 fun DeviceConfigurationOverride.Companion.DarkMode(
147     isDarkMode: Boolean,
148 ): DeviceConfigurationOverride = DeviceConfigurationOverride { contentUnderTest ->
149     OverriddenConfiguration(
150         configuration =
151             Configuration().apply {
152                 // Initialize from the current configuration
153                 updateFrom(LocalConfiguration.current)
154 
155                 // Override dark mode
156                 uiMode =
157                     uiMode and
158                         Configuration.UI_MODE_NIGHT_MASK.inv() or
159                         if (isDarkMode) {
160                             Configuration.UI_MODE_NIGHT_YES
161                         } else {
162                             Configuration.UI_MODE_NIGHT_NO
163                         }
164             },
165         content = contentUnderTest
166     )
167 }
168 
169 /**
170  * A [DeviceConfigurationOverride] that overrides the font weight adjustment for the contained
171  * content.
172  *
173  * @sample androidx.compose.ui.test.samples.DeviceConfigurationOverrideFontWeightAdjustmentSample
174  */
175 @RequiresApi(31)
FontWeightAdjustmentnull176 fun DeviceConfigurationOverride.Companion.FontWeightAdjustment(
177     fontWeightAdjustment: Int,
178 ): DeviceConfigurationOverride = DeviceConfigurationOverride { contentUnderTest ->
179     OverriddenConfiguration(
180         configuration =
181             Configuration().apply {
182                 // Initialize from the current configuration
183                 updateFrom(LocalConfiguration.current)
184 
185                 // Override fontWeightAdjustment
186                 this.fontWeightAdjustment = fontWeightAdjustment
187             },
188         content = contentUnderTest
189     )
190 }
191 
192 /**
193  * A [DeviceConfigurationOverride] that overrides whether the screen is round for the contained
194  * content.
195  *
196  * @sample androidx.compose.ui.test.samples.DeviceConfigurationOverrideRoundScreenSample
197  */
198 @RequiresApi(23)
RoundScreennull199 fun DeviceConfigurationOverride.Companion.RoundScreen(
200     isScreenRound: Boolean,
201 ): DeviceConfigurationOverride = DeviceConfigurationOverride { contentUnderTest ->
202     OverriddenConfiguration(
203         configuration =
204             Configuration().apply {
205                 // Initialize from the current configuration
206                 updateFrom(LocalConfiguration.current)
207 
208                 // Override isRound in screenLayout
209                 screenLayout =
210                     when (isScreenRound) {
211                         true ->
212                             (screenLayout and Configuration.SCREENLAYOUT_ROUND_MASK.inv()) or
213                                 Configuration.SCREENLAYOUT_ROUND_YES
214                         false ->
215                             (screenLayout and Configuration.SCREENLAYOUT_ROUND_MASK.inv()) or
216                                 Configuration.SCREENLAYOUT_ROUND_NO
217                     }
218             },
219         content = contentUnderTest
220     )
221 }
222 
223 /**
224  * A [DeviceConfigurationOverride] that overrides the window insets for the contained content.
225  *
226  * @sample androidx.compose.ui.test.samples.DeviceConfigurationOverrideWindowInsetsSample
227  */
WindowInsetsnull228 fun DeviceConfigurationOverride.Companion.WindowInsets(
229     windowInsets: WindowInsetsCompat,
230 ): DeviceConfigurationOverride = DeviceConfigurationOverride { contentUnderTest ->
231     val currentContentUnderTest by rememberUpdatedState(contentUnderTest)
232     val currentWindowInsets by rememberUpdatedState(windowInsets)
233     AndroidView(
234         factory = { context ->
235             object : AbstractComposeView(context) {
236                 @Composable
237                 override fun Content() {
238                     currentContentUnderTest()
239                 }
240 
241                 override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets {
242                     children.forEach {
243                         it.dispatchApplyWindowInsets(
244                             WindowInsets(currentWindowInsets.toWindowInsets())
245                         )
246                     }
247                     return WindowInsetsCompat.CONSUMED.toWindowInsets()!!
248                 }
249 
250                 /**
251                  * Deprecated, but intercept the `requestApplyInsets` call via the deprecated
252                  * method.
253                  */
254                 @Deprecated("Deprecated in Java")
255                 override fun requestFitSystemWindows() {
256                     dispatchApplyWindowInsets(WindowInsets(currentWindowInsets.toWindowInsets()!!))
257                 }
258             }
259         },
260         update = { with(currentWindowInsets) { it.requestApplyInsets() } }
261     )
262 }
263 
264 /**
265  * Overrides the compositions locals related to the given [configuration].
266  *
267  * There currently isn't a single source of truth for these values, so we update them all according
268  * to the given [configuration].
269  */
270 @Composable
OverriddenConfigurationnull271 private fun OverriddenConfiguration(configuration: Configuration, content: @Composable () -> Unit) {
272     // We don't override the theme, but we do want to override the configuration and this seems
273     // convenient to do so
274     val newContext =
275         ContextThemeWrapper(LocalContext.current, 0).apply {
276             applyOverrideConfiguration(configuration)
277         }
278 
279     CompositionLocalProvider(
280         LocalContext provides newContext,
281         LocalConfiguration provides configuration,
282         LocalLayoutDirection provides
283             if (configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) {
284                 LayoutDirection.Ltr
285             } else {
286                 LayoutDirection.Rtl
287             },
288         LocalDensity provides
289             Density(
290                 configuration.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT,
291                 configuration.fontScale
292             ),
293         LocalFontFamilyResolver provides createFontFamilyResolver(newContext),
294         content = content
295     )
296 }
297