<lambda>null1 // Copyright 2015-present 650 Industries. All rights reserved.
2 package host.exp.exponent.utils
3
4 import expo.modules.manifests.core.Manifest
5 import android.app.Activity
6 import android.content.pm.ActivityInfo
7 import android.view.WindowManager
8 import host.exp.exponent.ExponentManifest
9 import android.view.WindowInsets
10 import host.exp.exponent.ExponentManifest.BitmapListener
11 import android.graphics.Bitmap
12 import android.app.ActivityManager.TaskDescription
13 import android.graphics.Color
14 import android.os.Build
15 import android.view.View
16 import androidx.annotation.UiThread
17 import androidx.appcompat.app.AppCompatActivity
18 import androidx.appcompat.app.AppCompatDelegate
19 import androidx.core.view.ViewCompat
20 import expo.modules.jsonutils.getNullable
21 import host.exp.exponent.analytics.EXL
22
23 object ExperienceActivityUtils {
24 private val TAG = ExperienceActivityUtils::class.java.simpleName
25
26 private const val STATUS_BAR_STYLE_DARK_CONTENT = "dark-content"
27 private const val STATUS_BAR_STYLE_LIGHT_CONTENT = "light-content"
28
29 fun updateOrientation(manifest: Manifest, activity: Activity) {
30 val orientation = manifest.getOrientation()
31 if (orientation == null) {
32 activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
33 return
34 }
35 when (orientation) {
36 "default" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
37 "portrait" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
38 "landscape" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
39 }
40 }
41
42 fun updateSoftwareKeyboardLayoutMode(manifest: Manifest, activity: Activity) {
43 val keyboardLayoutMode = manifest.getAndroidKeyboardLayoutMode() ?: "resize"
44
45 // It's only necessary to set this manually for pan, resize is the default for the activity.
46 if (keyboardLayoutMode == "pan") {
47 activity.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN)
48 }
49 }
50
51 // region user interface style - light/dark/automatic mode
52 /**
53 * Sets uiMode to according to what is being set in manifest.
54 */
55 fun overrideUiMode(manifest: Manifest, activity: AppCompatActivity) {
56 val userInterfaceStyle = manifest.getAndroidUserInterfaceStyle() ?: "light"
57 activity.delegate.localNightMode = nightModeFromString(userInterfaceStyle)
58 }
59
60 private fun nightModeFromString(userInterfaceStyle: String?): Int {
61 return if (userInterfaceStyle == null) {
62 AppCompatDelegate.MODE_NIGHT_NO
63 } else when (userInterfaceStyle) {
64 "automatic" -> {
65 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
66 AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
67 } else AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
68 }
69 "dark" -> AppCompatDelegate.MODE_NIGHT_YES
70 "light" -> AppCompatDelegate.MODE_NIGHT_NO
71 else -> AppCompatDelegate.MODE_NIGHT_NO
72 }
73 }
74
75 // endregion
76
77 // region StatusBar configuration
78
79 /**
80 * React Native is not using flag [WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS] nor view/manifest attribute 'android:windowTranslucentStatus'
81 * (https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_TRANSLUCENT_STATUS)
82 * (https://developer.android.com/reference/android/R.attr.html#windowTranslucentStatus)
83 * Instead it's using [WindowInsets] to limit available space on the screen ([com.facebook.react.modules.statusbar.StatusBarModule.setTranslucent]).
84 *
85 *
86 * In case 'android:'windowTranslucentStatus' is used in activity's theme, it has to be removed in order to make RN's Status Bar API work.
87 * Out approach to achieve translucency of StatusBar has to be aligned with RN's approach to ensure [com.facebook.react.modules.statusbar.StatusBarModule] works.
88 *
89 *
90 * Links to follow in case of need of more detailed understating.
91 * https://chris.banes.dev/talks/2017/becoming-a-master-window-fitter-lon/
92 * https://www.youtube.com/watch?v=_mGDMVRO3iE
93 */
94 fun configureStatusBar(manifest: Manifest, activity: Activity) {
95 val statusBarOptions = manifest.getAndroidStatusBarOptions()
96 val statusBarStyle = statusBarOptions?.getNullable<String>(ExponentManifest.MANIFEST_STATUS_BAR_APPEARANCE)
97 val statusBarBackgroundColor = statusBarOptions?.getNullable<String>(ExponentManifest.MANIFEST_STATUS_BAR_BACKGROUND_COLOR)
98
99 val statusBarHidden = statusBarOptions != null && statusBarOptions.optBoolean(
100 ExponentManifest.MANIFEST_STATUS_BAR_HIDDEN,
101 false
102 )
103 val statusBarTranslucent = statusBarOptions == null || statusBarOptions.optBoolean(
104 ExponentManifest.MANIFEST_STATUS_BAR_TRANSLUCENT,
105 true
106 )
107
108 activity.runOnUiThread {
109 // clear android:windowTranslucentStatus flag from Window as RN achieves translucency using WindowInsets
110 activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
111
112 setHidden(statusBarHidden, activity)
113
114 setTranslucent(statusBarTranslucent, activity)
115
116 val appliedStatusBarStyle = setStyle(statusBarStyle, activity)
117
118 // Color passed from manifest is in format '#RRGGBB(AA)' and Android uses '#AARRGGBB'
119 val normalizedStatusBarBackgroundColor = RGBAtoARGB(statusBarBackgroundColor)
120
121 if (normalizedStatusBarBackgroundColor == null || !ColorParser.isValid(normalizedStatusBarBackgroundColor)) {
122 // backgroundColor is invalid or not set
123 if (appliedStatusBarStyle == STATUS_BAR_STYLE_LIGHT_CONTENT) {
124 // appliedStatusBarStyle is "light-content" so background color should be semi transparent black
125 setColor(Color.parseColor("#88000000"), activity)
126 } else {
127 // otherwise it has to be transparent
128 setColor(Color.TRANSPARENT, activity)
129 }
130 } else {
131 setColor(Color.parseColor(normalizedStatusBarBackgroundColor), activity)
132 }
133 }
134 }
135
136 /**
137 * If the string conforms to the "#RRGGBBAA" format then it's converted into the "#AARRGGBB" format.
138 * Otherwise noop.
139 */
140 private fun RGBAtoARGB(rgba: String?): String? {
141 if (rgba == null) {
142 return null
143 }
144 return if (rgba.startsWith("#") && rgba.length == 9) {
145 "#" + rgba.substring(7, 9) + rgba.substring(1, 7)
146 } else rgba
147 }
148
149 @UiThread
150 fun setColor(color: Int, activity: Activity) {
151 activity
152 .window
153 .addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
154 activity
155 .window.statusBarColor = color
156 }
157
158 @UiThread
159 fun setTranslucent(translucent: Boolean, activity: Activity) {
160 // If the status bar is translucent hook into the window insets calculations
161 // and consume all the top insets so no padding will be added under the status bar.
162 val decorView = activity.window.decorView
163 if (translucent) {
164 decorView.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets? ->
165 val defaultInsets = v.onApplyWindowInsets(insets)
166 defaultInsets.replaceSystemWindowInsets(
167 defaultInsets.systemWindowInsetLeft,
168 0,
169 defaultInsets.systemWindowInsetRight,
170 defaultInsets.systemWindowInsetBottom
171 )
172 }
173 } else {
174 decorView.setOnApplyWindowInsetsListener(null)
175 }
176 ViewCompat.requestApplyInsets(decorView)
177 }
178
179 /**
180 * @return Effective style that is actually applied to the status bar.
181 */
182 @UiThread
183 private fun setStyle(style: String?, activity: Activity): String {
184 var appliedStatusBarStyle = STATUS_BAR_STYLE_LIGHT_CONTENT
185 val decorView = activity.window.decorView
186 var systemUiVisibilityFlags = decorView.systemUiVisibility
187 when (style) {
188 STATUS_BAR_STYLE_LIGHT_CONTENT -> {
189 systemUiVisibilityFlags =
190 systemUiVisibilityFlags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
191 appliedStatusBarStyle = STATUS_BAR_STYLE_LIGHT_CONTENT
192 }
193 STATUS_BAR_STYLE_DARK_CONTENT -> {
194 systemUiVisibilityFlags = systemUiVisibilityFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
195 appliedStatusBarStyle = STATUS_BAR_STYLE_DARK_CONTENT
196 }
197 else -> {
198 systemUiVisibilityFlags = systemUiVisibilityFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
199 appliedStatusBarStyle = STATUS_BAR_STYLE_DARK_CONTENT
200 }
201 }
202 decorView.systemUiVisibility = systemUiVisibilityFlags
203
204 return appliedStatusBarStyle
205 }
206
207 @UiThread
208 private fun setHidden(hidden: Boolean, activity: Activity) {
209 if (hidden) {
210 activity.window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
211 activity.window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
212 } else {
213 activity.window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
214 activity.window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
215 }
216 }
217
218 // endregion
219
220 fun setTaskDescription(
221 exponentManifest: ExponentManifest,
222 manifest: Manifest,
223 activity: Activity
224 ) {
225 val iconUrl = manifest.getIconUrl()
226 val color = exponentManifest.getColorFromManifest(manifest)
227 exponentManifest.loadIconBitmap(
228 iconUrl,
229 object : BitmapListener {
230 override fun onLoadBitmap(bitmap: Bitmap?) {
231 // This if statement is only needed so the compiler doesn't show an error.
232 try {
233 activity.setTaskDescription(
234 TaskDescription(
235 manifest.getName() ?: "",
236 bitmap,
237 color
238 )
239 )
240 } catch (e: Throwable) {
241 EXL.e(TAG, e)
242 }
243 }
244 }
245 )
246 }
247
248 fun setNavigationBar(manifest: Manifest, activity: Activity) {
249 val navBarOptions = manifest.getAndroidNavigationBarOptions() ?: return
250
251 // Set background color of navigation bar
252 val navBarColor = navBarOptions.getNullable<String>(ExponentManifest.MANIFEST_NAVIGATION_BAR_BACKGROUND_COLOR)
253 if (navBarColor != null && ColorParser.isValid(navBarColor)) {
254 try {
255 activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
256 activity.window.navigationBarColor = Color.parseColor(navBarColor)
257 } catch (e: Throwable) {
258 EXL.e(TAG, e)
259 }
260 }
261
262 // Set icon color of navigation bar
263 val navBarAppearance = navBarOptions.getNullable<String>(ExponentManifest.MANIFEST_NAVIGATION_BAR_APPEARANCE)
264 if (navBarAppearance != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
265 try {
266 activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
267 activity.window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
268 if (navBarAppearance == "dark-content") {
269 val decorView = activity.window.decorView
270 var flags = decorView.systemUiVisibility
271 flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
272 decorView.systemUiVisibility = flags
273 }
274 } catch (e: Throwable) {
275 EXL.e(TAG, e)
276 }
277 }
278
279 // Set visibility of navigation bar
280 val navBarVisible = navBarOptions.getNullable<String>(ExponentManifest.MANIFEST_NAVIGATION_BAR_VISIBLILITY)
281 if (navBarVisible != null) {
282 // Hide both the navigation bar and the status bar. The Android docs recommend, "you should
283 // design your app to hide the status bar whenever you hide the navigation bar."
284 val decorView = activity.window.decorView
285 var flags = decorView.systemUiVisibility
286 when (navBarVisible) {
287 "leanback" ->
288 flags =
289 flags or (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN)
290 "immersive" ->
291 flags =
292 flags or (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE)
293 "sticky-immersive" ->
294 flags =
295 flags or (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
296 }
297 decorView.systemUiVisibility = flags
298 }
299 }
300
301 fun setRootViewBackgroundColor(manifest: Manifest, rootView: View) {
302 var colorString = manifest.getAndroidBackgroundColor()
303 if (colorString == null || !ColorParser.isValid(colorString)) {
304 colorString = "#ffffff"
305 }
306 try {
307 val color = Color.parseColor(colorString)
308 rootView.setBackgroundColor(color)
309 } catch (e: Throwable) {
310 EXL.e(TAG, e)
311 rootView.setBackgroundColor(Color.WHITE)
312 }
313 }
314 }
315