1 // 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 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 186 val decorView = activity.window.decorView 187 var systemUiVisibilityFlags = decorView.systemUiVisibility 188 when (style) { 189 STATUS_BAR_STYLE_LIGHT_CONTENT -> { 190 systemUiVisibilityFlags = 191 systemUiVisibilityFlags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() 192 appliedStatusBarStyle = STATUS_BAR_STYLE_LIGHT_CONTENT 193 } 194 STATUS_BAR_STYLE_DARK_CONTENT -> { 195 systemUiVisibilityFlags = systemUiVisibilityFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 196 appliedStatusBarStyle = STATUS_BAR_STYLE_DARK_CONTENT 197 } 198 else -> { 199 systemUiVisibilityFlags = systemUiVisibilityFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 200 appliedStatusBarStyle = STATUS_BAR_STYLE_DARK_CONTENT 201 } 202 } 203 decorView.systemUiVisibility = systemUiVisibilityFlags 204 } 205 return appliedStatusBarStyle 206 } 207 208 @UiThread 209 private fun setHidden(hidden: Boolean, activity: Activity) { 210 if (hidden) { 211 activity.window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) 212 activity.window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) 213 } else { 214 activity.window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) 215 activity.window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) 216 } 217 } 218 219 // endregion 220 221 fun setTaskDescription( 222 exponentManifest: ExponentManifest, 223 manifest: Manifest, 224 activity: Activity 225 ) { 226 val iconUrl = manifest.getIconUrl() 227 val color = exponentManifest.getColorFromManifest(manifest) 228 exponentManifest.loadIconBitmap( 229 iconUrl, 230 object : BitmapListener { 231 override fun onLoadBitmap(bitmap: Bitmap?) { 232 // This if statement is only needed so the compiler doesn't show an error. 233 try { 234 activity.setTaskDescription( 235 TaskDescription( 236 manifest.getName() ?: "", 237 bitmap, 238 color 239 ) 240 ) 241 } catch (e: Throwable) { 242 EXL.e(TAG, e) 243 } 244 } 245 } 246 ) 247 } 248 249 fun setNavigationBar(manifest: Manifest, activity: Activity) { 250 val navBarOptions = manifest.getAndroidNavigationBarOptions() ?: return 251 252 // Set background color of navigation bar 253 val navBarColor = navBarOptions.getNullable<String>(ExponentManifest.MANIFEST_NAVIGATION_BAR_BACKGROUND_COLOR) 254 if (navBarColor != null && ColorParser.isValid(navBarColor)) { 255 try { 256 activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) 257 activity.window.navigationBarColor = Color.parseColor(navBarColor) 258 } catch (e: Throwable) { 259 EXL.e(TAG, e) 260 } 261 } 262 263 // Set icon color of navigation bar 264 val navBarAppearance = navBarOptions.getNullable<String>(ExponentManifest.MANIFEST_NAVIGATION_BAR_APPEARANCE) 265 if (navBarAppearance != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 266 try { 267 activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) 268 activity.window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) 269 if (navBarAppearance == "dark-content") { 270 val decorView = activity.window.decorView 271 var flags = decorView.systemUiVisibility 272 flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR 273 decorView.systemUiVisibility = flags 274 } 275 } catch (e: Throwable) { 276 EXL.e(TAG, e) 277 } 278 } 279 280 // Set visibility of navigation bar 281 val navBarVisible = navBarOptions.getNullable<String>(ExponentManifest.MANIFEST_NAVIGATION_BAR_VISIBLILITY) 282 if (navBarVisible != null) { 283 // Hide both the navigation bar and the status bar. The Android docs recommend, "you should 284 // design your app to hide the status bar whenever you hide the navigation bar." 285 val decorView = activity.window.decorView 286 var flags = decorView.systemUiVisibility 287 when (navBarVisible) { 288 "leanback" -> 289 flags = 290 flags or (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN) 291 "immersive" -> 292 flags = 293 flags or (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE) 294 "sticky-immersive" -> 295 flags = 296 flags or (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) 297 } 298 decorView.systemUiVisibility = flags 299 } 300 } 301 302 fun setRootViewBackgroundColor(manifest: Manifest, rootView: View) { 303 var colorString = manifest.getAndroidBackgroundColor() 304 if (colorString == null || !ColorParser.isValid(colorString)) { 305 colorString = "#ffffff" 306 } 307 try { 308 val color = Color.parseColor(colorString) 309 rootView.setBackgroundColor(color) 310 } catch (e: Throwable) { 311 EXL.e(TAG, e) 312 rootView.setBackgroundColor(Color.WHITE) 313 } 314 } 315 } 316