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