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 host.exp.exponent.analytics.EXL 21 22 object ExperienceActivityUtils { 23 private val TAG = ExperienceActivityUtils::class.java.simpleName 24 25 private const val STATUS_BAR_STYLE_DARK_CONTENT = "dark-content" 26 private const val STATUS_BAR_STYLE_LIGHT_CONTENT = "light-content" 27 28 fun updateOrientation(manifest: Manifest, activity: Activity) { 29 val orientation = manifest.getOrientation() 30 if (orientation == null) { 31 activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED 32 return 33 } 34 when (orientation) { 35 "default" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED 36 "portrait" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT 37 "landscape" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE 38 } 39 } 40 41 fun updateSoftwareKeyboardLayoutMode(manifest: Manifest, activity: Activity) { 42 val keyboardLayoutMode = manifest.getAndroidKeyboardLayoutMode() ?: "resize" 43 44 // It's only necessary to set this manually for pan, resize is the default for the activity. 45 if (keyboardLayoutMode == "pan") { 46 activity.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) 47 } 48 } 49 50 // region user interface style - light/dark/automatic mode 51 /** 52 * Sets uiMode to according to what is being set in manifest. 53 */ 54 fun overrideUiMode(manifest: Manifest, activity: AppCompatActivity) { 55 val userInterfaceStyle = manifest.getAndroidUserInterfaceStyle() ?: "light" 56 activity.delegate.localNightMode = nightModeFromString(userInterfaceStyle) 57 } 58 59 private fun nightModeFromString(userInterfaceStyle: String?): Int { 60 return if (userInterfaceStyle == null) { 61 AppCompatDelegate.MODE_NIGHT_NO 62 } else when (userInterfaceStyle) { 63 "automatic" -> { 64 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { 65 AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY 66 } else AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM 67 } 68 "dark" -> AppCompatDelegate.MODE_NIGHT_YES 69 "light" -> AppCompatDelegate.MODE_NIGHT_NO 70 else -> AppCompatDelegate.MODE_NIGHT_NO 71 } 72 } 73 74 // endregion 75 76 // region StatusBar configuration 77 78 /** 79 * React Native is not using flag [WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS] nor view/manifest attribute 'android:windowTranslucentStatus' 80 * (https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_TRANSLUCENT_STATUS) 81 * (https://developer.android.com/reference/android/R.attr.html#windowTranslucentStatus) 82 * Instead it's using [WindowInsets] to limit available space on the screen ([com.facebook.react.modules.statusbar.StatusBarModule.setTranslucent]). 83 * 84 * 85 * 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. 86 * Out approach to achieve translucency of StatusBar has to be aligned with RN's approach to ensure [com.facebook.react.modules.statusbar.StatusBarModule] works. 87 * 88 * 89 * Links to follow in case of need of more detailed understating. 90 * https://chris.banes.dev/talks/2017/becoming-a-master-window-fitter-lon/ 91 * https://www.youtube.com/watch?v=_mGDMVRO3iE 92 */ 93 fun configureStatusBar(manifest: Manifest, activity: Activity) { 94 val statusBarOptions = manifest.getAndroidStatusBarOptions() 95 val statusBarStyle = statusBarOptions?.optString(ExponentManifest.MANIFEST_STATUS_BAR_APPEARANCE) 96 val statusBarBackgroundColor = statusBarOptions?.optString(ExponentManifest.MANIFEST_STATUS_BAR_BACKGROUND_COLOR, null) 97 98 val statusBarHidden = statusBarOptions != null && statusBarOptions.optBoolean( 99 ExponentManifest.MANIFEST_STATUS_BAR_HIDDEN, 100 false 101 ) 102 val statusBarTranslucent = statusBarOptions == null || statusBarOptions.optBoolean( 103 ExponentManifest.MANIFEST_STATUS_BAR_TRANSLUCENT, 104 true 105 ) 106 107 activity.runOnUiThread { 108 // clear android:windowTranslucentStatus flag from Window as RN achieves translucency using WindowInsets 109 activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) 110 111 setHidden(statusBarHidden, activity) 112 113 setTranslucent(statusBarTranslucent, activity) 114 115 val appliedStatusBarStyle = setStyle(statusBarStyle, activity) 116 117 // Color passed from manifest is in format '#RRGGBB(AA)' and Android uses '#AARRGGBB' 118 val normalizedStatusBarBackgroundColor = RGBAtoARGB(statusBarBackgroundColor) 119 120 if (normalizedStatusBarBackgroundColor == null || !ColorParser.isValid(normalizedStatusBarBackgroundColor)) { 121 // backgroundColor is invalid or not set 122 if (appliedStatusBarStyle == STATUS_BAR_STYLE_LIGHT_CONTENT) { 123 // appliedStatusBarStyle is "light-content" so background color should be semi transparent black 124 setColor(Color.parseColor("#88000000"), activity) 125 } else { 126 // otherwise it has to be transparent 127 setColor(Color.TRANSPARENT, activity) 128 } 129 } else { 130 setColor(Color.parseColor(normalizedStatusBarBackgroundColor), activity) 131 } 132 } 133 } 134 135 /** 136 * If the string conforms to the "#RRGGBBAA" format then it's converted into the "#AARRGGBB" format. 137 * Otherwise noop. 138 */ 139 private fun RGBAtoARGB(rgba: String?): String? { 140 if (rgba == null) { 141 return null 142 } 143 return if (rgba.startsWith("#") && rgba.length == 9) { 144 "#" + rgba.substring(7, 9) + rgba.substring(1, 7) 145 } else rgba 146 } 147 148 @UiThread 149 fun setColor(color: Int, activity: Activity) { 150 activity 151 .window 152 .addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) 153 activity 154 .window.statusBarColor = color 155 } 156 157 @UiThread 158 fun setTranslucent(translucent: Boolean, activity: Activity) { 159 // If the status bar is translucent hook into the window insets calculations 160 // and consume all the top insets so no padding will be added under the status bar. 161 val decorView = activity.window.decorView 162 if (translucent) { 163 decorView.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets? -> 164 val defaultInsets = v.onApplyWindowInsets(insets) 165 defaultInsets.replaceSystemWindowInsets( 166 defaultInsets.systemWindowInsetLeft, 167 0, 168 defaultInsets.systemWindowInsetRight, 169 defaultInsets.systemWindowInsetBottom 170 ) 171 } 172 } else { 173 decorView.setOnApplyWindowInsetsListener(null) 174 } 175 ViewCompat.requestApplyInsets(decorView) 176 } 177 178 /** 179 * @return Effective style that is actually applied to the status bar. 180 */ 181 @UiThread 182 private fun setStyle(style: String?, activity: Activity): String { 183 var appliedStatusBarStyle = STATUS_BAR_STYLE_LIGHT_CONTENT 184 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 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 = if (navBarOptions.has(ExponentManifest.MANIFEST_NAVIGATION_BAR_BACKGROUND_COLOR)) { 253 navBarOptions.optString(ExponentManifest.MANIFEST_NAVIGATION_BAR_BACKGROUND_COLOR) 254 } else null 255 if (navBarColor != null && ColorParser.isValid(navBarColor)) { 256 try { 257 activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) 258 activity.window.navigationBarColor = Color.parseColor(navBarColor) 259 } catch (e: Throwable) { 260 EXL.e(TAG, e) 261 } 262 } 263 264 // Set icon color of navigation bar 265 val navBarAppearance = if (navBarOptions.has(ExponentManifest.MANIFEST_NAVIGATION_BAR_APPEARANCE)) { 266 navBarOptions.optString(ExponentManifest.MANIFEST_NAVIGATION_BAR_APPEARANCE) 267 } else null 268 if (navBarAppearance != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 269 try { 270 activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) 271 activity.window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) 272 if (navBarAppearance == "dark-content") { 273 val decorView = activity.window.decorView 274 var flags = decorView.systemUiVisibility 275 flags = flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR 276 decorView.systemUiVisibility = flags 277 } 278 } catch (e: Throwable) { 279 EXL.e(TAG, e) 280 } 281 } 282 283 // Set visibility of navigation bar 284 val navBarVisible = if (navBarOptions.has(ExponentManifest.MANIFEST_NAVIGATION_BAR_VISIBLILITY)) { 285 navBarOptions.optString(ExponentManifest.MANIFEST_NAVIGATION_BAR_VISIBLILITY) 286 } else null 287 if (navBarVisible != null) { 288 // Hide both the navigation bar and the status bar. The Android docs recommend, "you should 289 // design your app to hide the status bar whenever you hide the navigation bar." 290 val decorView = activity.window.decorView 291 var flags = decorView.systemUiVisibility 292 when (navBarVisible) { 293 "leanback" -> 294 flags = 295 flags or (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN) 296 "immersive" -> 297 flags = 298 flags or (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE) 299 "sticky-immersive" -> 300 flags = 301 flags or (View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) 302 } 303 decorView.systemUiVisibility = flags 304 } 305 } 306 307 fun setRootViewBackgroundColor(manifest: Manifest, rootView: View) { 308 var colorString = manifest.getAndroidBackgroundColor() 309 if (colorString == null || !ColorParser.isValid(colorString)) { 310 colorString = "#ffffff" 311 } 312 try { 313 val color = Color.parseColor(colorString) 314 rootView.setBackgroundColor(color) 315 } catch (e: Throwable) { 316 EXL.e(TAG, e) 317 rootView.setBackgroundColor(Color.WHITE) 318 } 319 } 320 } 321