1 // Copyright 2015-present 650 Industries. All rights reserved. 2 package host.exp.exponent 3 4 import android.content.Context 5 import android.graphics.Bitmap 6 import android.graphics.BitmapFactory 7 import android.graphics.Color 8 import android.net.Uri 9 import android.os.AsyncTask 10 import android.os.Debug 11 import android.text.TextUtils 12 import android.util.Log 13 import android.util.LruCache 14 import expo.modules.updates.manifest.ManifestFactory 15 import expo.modules.updates.manifest.raw.InternalJSONMutator 16 import expo.modules.updates.manifest.raw.RawManifest 17 import host.exp.exponent.analytics.Analytics 18 import host.exp.exponent.analytics.EXL 19 import host.exp.exponent.exceptions.ManifestException 20 import host.exp.exponent.generated.ExponentBuildConstants 21 import host.exp.exponent.kernel.Crypto 22 import host.exp.exponent.kernel.ExponentUrls 23 import host.exp.exponent.kernel.KernelProvider 24 import host.exp.exponent.network.ExpoHeaders 25 import host.exp.exponent.network.ExpoResponse 26 import host.exp.exponent.network.ExponentHttpClient.SafeCallback 27 import host.exp.exponent.network.ExponentNetwork 28 import host.exp.exponent.storage.ExponentSharedPreferences 29 import host.exp.exponent.utils.ColorParser 30 import host.exp.expoview.R 31 import okhttp3.CacheControl 32 import org.apache.commons.io.IOUtils 33 import org.json.JSONArray 34 import org.json.JSONException 35 import org.json.JSONObject 36 import java.io.IOException 37 import java.net.HttpURLConnection 38 import java.net.URI 39 import java.net.URISyntaxException 40 import java.net.URL 41 import java.text.DateFormat 42 import java.text.ParseException 43 import java.text.SimpleDateFormat 44 import java.util.* 45 import javax.inject.Inject 46 import javax.inject.Singleton 47 import kotlin.math.max 48 49 @Singleton 50 class ExponentManifest @Inject constructor( 51 var context: Context, 52 var exponentNetwork: ExponentNetwork, 53 var crypto: Crypto, 54 var exponentSharedPreferences: ExponentSharedPreferences 55 ) { 56 interface ManifestListener { 57 fun onCompleted(manifest: RawManifest) 58 fun onError(e: Exception) 59 fun onError(e: String) 60 } 61 62 interface BitmapListener { 63 fun onLoadBitmap(bitmap: Bitmap?) 64 } 65 66 private val memoryCache: LruCache<String, Bitmap> 67 68 fun httpManifestUrl(manifestUrl: String): Uri { 69 return httpManifestUrlBuilder(manifestUrl).build() 70 } 71 72 private fun httpManifestUrlBuilder(manifestUrl: String): Uri.Builder { 73 var realManifestUrl = manifestUrl 74 if (manifestUrl.contains(REDIRECT_SNIPPET)) { 75 // Redirect urls look like "https://exp.host/--/to-exp/exp%3A%2F%2Fgj-5x6.jesse.internal.exp.direct%3A80". 76 // Android is crazy and catches this url with this intent filter: 77 // <data 78 // android:host="*.exp.direct" 79 // android:pathPattern=".*" 80 // android:scheme="http"/> 81 // <data 82 // android:host="*.exp.direct" 83 // android:pathPattern=".*" 84 // android:scheme="https"/> 85 // so we have to add some special logic to handle that. This is than handling arbitrary HTTP 301s and 302 86 realManifestUrl = Uri.decode( 87 realManifestUrl.substring( 88 realManifestUrl.indexOf( 89 REDIRECT_SNIPPET 90 ) + REDIRECT_SNIPPET.length 91 ) 92 ) 93 } 94 val httpManifestUrl = ExponentUrls.toHttp(realManifestUrl) 95 val uri = Uri.parse(httpManifestUrl) 96 var newPath = uri.path 97 if (newPath == null) { 98 newPath = "" 99 } 100 val deepLinkIndex = newPath.indexOf(DEEP_LINK_SEPARATOR_WITH_SLASH) 101 if (deepLinkIndex > -1) { 102 newPath = newPath.substring(0, deepLinkIndex) 103 } 104 return uri.buildUpon().encodedPath(newPath) 105 } 106 107 @JvmOverloads 108 fun fetchManifest( 109 manifestUrl: String, 110 listener: ManifestListener, 111 shouldWriteToCache: Boolean = true 112 ) { 113 Analytics.markEvent(Analytics.TimedEvent.STARTED_FETCHING_MANIFEST) 114 val uriBuilder = httpManifestUrlBuilder(manifestUrl) 115 if (!shouldWriteToCache) { 116 // add a dummy parameter so this doesn't overwrite the current cached manifest 117 // more correct would be to add Cache-Control: no-store header, but this doesn't seem to 118 // work correctly with requests in okhttp 119 uriBuilder.appendQueryParameter("cache", "false") 120 } 121 val httpManifestUrl = uriBuilder.build().toString() 122 123 // Fetch manifest 124 val requestBuilder = ExponentUrls.addExponentHeadersToManifestUrl( 125 httpManifestUrl, 126 manifestUrl == Constants.INITIAL_URL, 127 exponentSharedPreferences.sessionSecret 128 ).apply { 129 header("Exponent-Accept-Signature", "true") 130 header("Expo-JSON-Error", "true") 131 cacheControl(CacheControl.FORCE_NETWORK) 132 } 133 Analytics.markEvent(Analytics.TimedEvent.STARTED_MANIFEST_NETWORK_REQUEST) 134 if (Constants.DEBUG_MANIFEST_METHOD_TRACING) { 135 Debug.startMethodTracing("manifest") 136 } 137 val request = requestBuilder.build() 138 val finalUri = request.url().toString() 139 exponentNetwork.client.callSafe( 140 request, 141 object : SafeCallback { 142 override fun onFailure(e: IOException) { 143 listener.onError(ManifestException(e, manifestUrl)) 144 } 145 146 override fun onResponse(response: ExpoResponse) { 147 // OkHttp sometimes decides to use the cache anyway here 148 val isCached = response.networkResponse() == null 149 handleManifestResponse(response, manifestUrl, finalUri, listener, false, isCached) 150 } 151 152 override fun onCachedResponse(response: ExpoResponse, isEmbedded: Boolean) { 153 // this is only called if network is unavailable for some reason 154 handleManifestResponse(response, manifestUrl, finalUri, listener, isEmbedded, true) 155 } 156 } 157 ) 158 } 159 160 private fun handleManifestResponse( 161 response: ExpoResponse, 162 manifestUrl: String, 163 httpManifestUrl: String, 164 listener: ManifestListener, 165 isEmbedded: Boolean, 166 isCached: Boolean 167 ) { 168 if (!response.isSuccessful) { 169 val exception: ManifestException = try { 170 val errorJSON = JSONObject(response.body().string()) 171 ManifestException(null, manifestUrl, errorJSON) 172 } catch (e: JSONException) { 173 ManifestException(null, manifestUrl) 174 } catch (e: IOException) { 175 ManifestException(null, manifestUrl) 176 } 177 listener.onError(exception) 178 return 179 } 180 try { 181 val manifestString = response.body().string() 182 fetchManifestStep2( 183 manifestUrl, 184 httpManifestUrl, 185 manifestString, 186 response.headers(), 187 listener, 188 isEmbedded, 189 isCached 190 ) 191 } catch (e: JSONException) { 192 listener.onError(e) 193 } catch (e: IOException) { 194 listener.onError(e) 195 } catch (e: URISyntaxException) { 196 listener.onError(e) 197 } 198 } 199 200 @Throws(JSONException::class, ParseException::class) 201 private fun newerManifest(manifest1: RawManifest, manifest2: RawManifest): RawManifest { 202 val manifest1Timestamp = manifest1.getSortTime() 203 val manifest2Timestamp = manifest2.getSortTime() 204 205 // SimpleDateFormat on Android does not support the ISO-8601 representation of the timezone, 206 // namely, using 'Z' to represent GMT. Since all our dates here are in the same timezone, 207 // and we're just comparing them relative to each other, we can just ignore this character. 208 val formatter: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) 209 val manifest1Date = formatter.parse(manifest1Timestamp) 210 val manifest2Date = formatter.parse(manifest2Timestamp) 211 return if (manifest1Date.after(manifest2Date)) { 212 manifest1 213 } else { 214 manifest2 215 } 216 } 217 218 private fun isManifestSDKVersionValid(manifest: RawManifest): Boolean { 219 val sdkVersion = manifest.getSDKVersionNullable() ?: return false 220 return if (RNObject.UNVERSIONED == sdkVersion) { 221 true 222 } else { 223 for (version in Constants.SDK_VERSIONS_LIST) { 224 if (version == sdkVersion) { 225 return true 226 } 227 } 228 false 229 } 230 } 231 232 @Throws(IOException::class) 233 private fun extractManifest(manifestString: String): JSONObject { 234 try { 235 return JSONObject(manifestString) 236 } catch (e: JSONException) { 237 // Ignore this error, try to parse manifest as array 238 } 239 try { 240 // the manifestString could be an array of manifest objects 241 // in this case, we choose the first compatible manifest in the array 242 val manifestArray = JSONArray(manifestString) 243 for (i in 0 until manifestArray.length()) { 244 val manifestCandidate = manifestArray.getJSONObject(i) 245 val sdkVersion = manifestCandidate.getString(MANIFEST_SDK_VERSION_KEY) 246 if (Constants.SDK_VERSIONS_LIST.contains(sdkVersion)) { 247 return manifestCandidate 248 } 249 } 250 } catch (e: JSONException) { 251 throw IOException( 252 "Manifest string is not a valid JSONObject or JSONArray: $manifestString", 253 e 254 ) 255 } 256 throw IOException("No compatible manifest found. SDK Versions supported: " + Constants.SDK_VERSIONS + " Provided manifestString: " + manifestString) 257 } 258 259 @Throws(JSONException::class, URISyntaxException::class, IOException::class) 260 private fun fetchManifestStep2( 261 manifestUrl: String, 262 httpManifestUrl: String, 263 manifestString: String, 264 headers: ExpoHeaders?, 265 listener: ManifestListener, 266 isEmbedded: Boolean, 267 isCached: Boolean 268 ) { 269 if (Constants.DEBUG_MANIFEST_METHOD_TRACING) { 270 Debug.stopMethodTracing() 271 } 272 if (headers != null) { 273 Analytics.markEvent(Analytics.TimedEvent.FINISHED_MANIFEST_NETWORK_REQUEST) 274 } 275 val outerManifestJson = extractManifest(manifestString) 276 val isMainShellAppExperience = manifestUrl == Constants.INITIAL_URL 277 val parsedManifestUrl = URI(manifestUrl) 278 val isManifestSigned = outerManifestJson.has(MANIFEST_STRING_KEY) && outerManifestJson.has( 279 MANIFEST_SIGNATURE_KEY 280 ) 281 var manifestJson = outerManifestJson 282 if (isManifestSigned) { 283 // get inner manifest if manifest is wrapped in signature 284 manifestJson = JSONObject(outerManifestJson.getString(MANIFEST_STRING_KEY)) 285 } 286 var manifest = ManifestFactory.getRawManifestFromJson( 287 manifestJson 288 ) 289 290 // if the manifest we are passed is from the cache, we need to get the embedded manifest so that 291 // we can compare them in case embedded manifest is newer (i.e. user has installed a new APK) 292 var isUsingEmbeddedManifest = isEmbedded 293 if (!isEmbedded && isCached) { 294 val embeddedResponse = exponentNetwork.client.getHardCodedResponse(httpManifestUrl) 295 if (embeddedResponse != null) { 296 try { 297 val embeddedManifest = ManifestFactory.getRawManifestFromJson(JSONObject(embeddedResponse)) 298 manifest = if (!isManifestSDKVersionValid(manifest)) { 299 // if we somehow try to load a cached manifest with an invalid SDK version, 300 // fall back immediately to the embedded manifest, which should never have an 301 // invalid SDK version. 302 embeddedManifest 303 } else { 304 newerManifest(embeddedManifest, manifest) 305 } 306 isUsingEmbeddedManifest = embeddedManifest === manifest 307 } catch (e: Exception) { 308 EXL.e(TAG, e) 309 } 310 } 311 } 312 val isUsingEmbeddedManifestFinal = isUsingEmbeddedManifest 313 manifest.mutateInternalJSONInPlace(object : InternalJSONMutator { 314 override fun updateJSON(json: JSONObject) { 315 json.put( 316 MANIFEST_LOADED_FROM_CACHE_KEY, isCached || isUsingEmbeddedManifestFinal 317 ) 318 } 319 }) 320 if (isManifestSigned) { 321 val isOffline = !ExponentNetwork.isNetworkAvailable(context) 322 if (isAnonymousExperience(manifest) || isMainShellAppExperience || isUsingEmbeddedManifest) { 323 // Automatically verified. 324 fetchManifestStep3(manifest, true, listener) 325 } else { 326 val finalManifest = manifest 327 crypto.verifyPublicRSASignature( 328 Constants.API_HOST + "/--/manifest-public-key", 329 outerManifestJson.getString(MANIFEST_STRING_KEY), 330 outerManifestJson.getString( 331 MANIFEST_SIGNATURE_KEY 332 ), 333 object : Crypto.RSASignatureListener { 334 override fun onError(errorMessage: String?, isNetworkError: Boolean) { 335 if (isOffline && isNetworkError) { 336 // automatically validate if offline and don't have public key 337 // TODO: we need to evict manifest from the cache if it doesn't pass validation when online 338 fetchManifestStep3(finalManifest, true, listener) 339 } else { 340 Log.w(TAG, errorMessage!!) 341 fetchManifestStep3(finalManifest, false, listener) 342 } 343 } 344 345 override fun onCompleted(isValid: Boolean) { 346 fetchManifestStep3(finalManifest, isValid, listener) 347 } 348 } 349 ) 350 } 351 } else { 352 // if we're using a cached manifest that's stored without the signature, we can assume 353 // we've already verified it previously 354 if (isCached || isUsingEmbeddedManifest || isMainShellAppExperience) { 355 fetchManifestStep3(manifest, true, listener) 356 } else if (isThirdPartyHosted(parsedManifestUrl)) { 357 // Sandbox third party apps and consider them verified 358 // for https urls, sandboxed id is of form quinlanj.github.io/myProj-myApp 359 // for http urls, sandboxed id is of form UNVERIFIED-quinlanj.github.io/myProj-myApp 360 if (!Constants.isStandaloneApp()) { 361 val protocol = parsedManifestUrl.scheme 362 val securityPrefix = if (protocol == "https" || protocol == "exps") "" else "UNVERIFIED-" 363 val path = if (parsedManifestUrl.path != null) parsedManifestUrl.path else "" 364 val slug = if (manifest.getSlug() != null) manifest.getSlug() else "" 365 val sandboxedId = securityPrefix + parsedManifestUrl.host + path + "-" + slug 366 manifest.mutateInternalJSONInPlace(object : InternalJSONMutator { 367 override fun updateJSON(json: JSONObject) { 368 json.put( 369 MANIFEST_ID_KEY, sandboxedId 370 ) 371 } 372 }) 373 } 374 fetchManifestStep3(manifest, true, listener) 375 } else { 376 fetchManifestStep3(manifest, false, listener) 377 } 378 } 379 if (headers != null) { 380 val exponentServerHeader = headers[EXPONENT_SERVER_HEADER] 381 if (exponentServerHeader != null) { 382 try { 383 val eventProperties = JSONObject(exponentServerHeader) 384 Analytics.logEvent(Analytics.LOAD_DEVELOPER_MANIFEST, eventProperties) 385 } catch (e: Throwable) { 386 EXL.e(TAG, e) 387 } 388 } 389 } 390 } 391 392 private fun isThirdPartyHosted(uri: URI): Boolean { 393 val host = uri.host 394 val isExpoHost = 395 host == "exp.host" || host == "expo.io" || host == "exp.direct" || host == "expo.test" || 396 host.endsWith(".exp.host") || host.endsWith(".expo.io") || host.endsWith(".exp.direct") || host.endsWith( 397 ".expo.test" 398 ) 399 return !isExpoHost 400 } 401 402 private fun fetchManifestStep3( 403 manifest: RawManifest, 404 isVerified: Boolean, 405 listener: ManifestListener 406 ) { 407 try { 408 manifest.getBundleURL() 409 } catch (e: JSONException) { 410 listener.onError("No bundleUrl in manifest") 411 return 412 } 413 try { 414 manifest.mutateInternalJSONInPlace(object : InternalJSONMutator { 415 override fun updateJSON(json: JSONObject) { 416 json.put( 417 MANIFEST_IS_VERIFIED_KEY, isVerified 418 ) 419 } 420 }) 421 } catch (e: JSONException) { 422 listener.onError(e) 423 return 424 } 425 listener.onCompleted(manifest) 426 } 427 428 fun loadIconBitmap(iconUrl: String?, listener: BitmapListener) { 429 val icon = getIconFromCache(iconUrl) 430 if (icon != null) { 431 listener.onLoadBitmap(icon) 432 return 433 } 434 object : AsyncTask<Void?, Void?, Bitmap>() { 435 override fun doInBackground(vararg p0: Void?): Bitmap? { 436 return loadIconTask(iconUrl) 437 } 438 439 override fun onPostExecute(result: Bitmap?) { 440 listener.onLoadBitmap(result) 441 } 442 }.execute() 443 } 444 445 private fun getIconFromCache(iconUrl: String?): Bitmap? { 446 return if (iconUrl == null || TextUtils.isEmpty(iconUrl)) { 447 BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher) 448 } else memoryCache[iconUrl] 449 } 450 451 private fun loadIconTask(iconUrl: String?): Bitmap? { 452 return try { 453 // TODO: inject shared OkHttp client 454 val url = URL(iconUrl) 455 val connection = url.openConnection() as HttpURLConnection 456 connection.doInput = true 457 connection.connect() 458 val input = connection.inputStream 459 val bitmap = BitmapFactory.decodeStream(input) 460 val width = bitmap.width 461 val height = bitmap.height 462 if (width <= MAX_BITMAP_SIZE && height <= MAX_BITMAP_SIZE) { 463 memoryCache.put(iconUrl, bitmap) 464 return bitmap 465 } 466 val maxDimension = max(width, height) 467 val scaledWidth = width.toFloat() * MAX_BITMAP_SIZE / maxDimension 468 val scaledHeight = height.toFloat() * MAX_BITMAP_SIZE / maxDimension 469 val scaledBitmap = 470 Bitmap.createScaledBitmap(bitmap, scaledWidth.toInt(), scaledHeight.toInt(), true) 471 memoryCache.put(iconUrl, scaledBitmap) 472 scaledBitmap 473 } catch (e: IOException) { 474 EXL.e(TAG, e) 475 BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher) 476 } catch (e: Throwable) { 477 EXL.e(TAG, e) 478 BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher) 479 } 480 } 481 482 fun getColorFromManifest(manifest: RawManifest): Int { 483 val colorString = manifest.getPrimaryColor() 484 return if (colorString != null && ColorParser.isValid(colorString)) { 485 Color.parseColor(colorString) 486 } else { 487 R.color.colorPrimary 488 } 489 } 490 491 fun isAnonymousExperience(manifest: RawManifest): Boolean { 492 return try { 493 val id = manifest.getLegacyID() 494 id.startsWith(ANONYMOUS_EXPERIENCE_PREFIX) 495 } catch (e: JSONException) { 496 false 497 } 498 } 499 500 private fun getLocalKernelManifest(): RawManifest = try { 501 val manifest = JSONObject(ExponentBuildConstants.BUILD_MACHINE_KERNEL_MANIFEST) 502 manifest.put(MANIFEST_IS_VERIFIED_KEY, true) 503 ManifestFactory.getRawManifestFromJson(manifest) 504 } catch (e: JSONException) { 505 throw RuntimeException("Can't get local manifest: $e") 506 } 507 508 private fun getRemoteKernelManifest(): RawManifest? = try { 509 val inputStream = context.assets.open(EMBEDDED_KERNEL_MANIFEST_ASSET) 510 val jsonString = IOUtils.toString(inputStream) 511 val manifest = JSONObject(jsonString) 512 manifest.put(MANIFEST_IS_VERIFIED_KEY, true) 513 ManifestFactory.getRawManifestFromJson(manifest) 514 } catch (e: Exception) { 515 KernelProvider.instance.handleError(e) 516 null 517 } 518 519 fun getKernelManifest(): RawManifest { 520 val manifest: RawManifest? 521 val log: String 522 if (exponentSharedPreferences.shouldUseInternetKernel()) { 523 log = "Using remote Expo kernel manifest" 524 manifest = getRemoteKernelManifest() 525 } else { 526 log = "Using local Expo kernel manifest" 527 manifest = getLocalKernelManifest() 528 } 529 if (!hasShownKernelManifestLog) { 530 hasShownKernelManifestLog = true 531 EXL.d(TAG, log + ": " + manifest.toString()) 532 } 533 return manifest!! 534 } 535 536 companion object { 537 private val TAG = ExponentManifest::class.java.simpleName 538 539 const val MANIFEST_STRING_KEY = "manifestString" 540 const val MANIFEST_SIGNATURE_KEY = "signature" 541 const val MANIFEST_ID_KEY = "id" 542 const val MANIFEST_NAME_KEY = "name" 543 const val MANIFEST_APP_KEY_KEY = "appKey" 544 const val MANIFEST_SDK_VERSION_KEY = "sdkVersion" 545 const val MANIFEST_IS_VERIFIED_KEY = "isVerified" 546 const val MANIFEST_ICON_URL_KEY = "iconUrl" 547 const val MANIFEST_BACKGROUND_COLOR_KEY = "backgroundColor" 548 const val MANIFEST_PRIMARY_COLOR_KEY = "primaryColor" 549 const val MANIFEST_ORIENTATION_KEY = "orientation" 550 const val MANIFEST_DEVELOPER_KEY = "developer" 551 const val MANIFEST_DEVELOPER_TOOL_KEY = "tool" 552 const val MANIFEST_PACKAGER_OPTS_KEY = "packagerOpts" 553 const val MANIFEST_PACKAGER_OPTS_DEV_KEY = "dev" 554 const val MANIFEST_BUNDLE_URL_KEY = "bundleUrl" 555 const val MANIFEST_REVISION_ID_KEY = "revisionId" 556 const val MANIFEST_PUBLISHED_TIME_KEY = "publishedTime" 557 const val MANIFEST_COMMIT_TIME_KEY = "commitTime" 558 const val MANIFEST_LOADED_FROM_CACHE_KEY = "loadedFromCache" 559 const val MANIFEST_SLUG = "slug" 560 const val MANIFEST_ANDROID_INFO_KEY = "android" 561 const val MANIFEST_KEYBOARD_LAYOUT_MODE_KEY = "softwareKeyboardLayoutMode" 562 563 // Statusbar 564 const val MANIFEST_STATUS_BAR_KEY = "androidStatusBar" 565 const val MANIFEST_STATUS_BAR_APPEARANCE = "barStyle" 566 const val MANIFEST_STATUS_BAR_BACKGROUND_COLOR = "backgroundColor" 567 const val MANIFEST_STATUS_BAR_HIDDEN = "hidden" 568 const val MANIFEST_STATUS_BAR_TRANSLUCENT = "translucent" 569 570 // NavigationBar 571 const val MANIFEST_NAVIGATION_BAR_KEY = "androidNavigationBar" 572 const val MANIFEST_NAVIGATION_BAR_VISIBLILITY = "visible" 573 const val MANIFEST_NAVIGATION_BAR_APPEARANCE = "barStyle" 574 const val MANIFEST_NAVIGATION_BAR_BACKGROUND_COLOR = "backgroundColor" 575 576 // Notification 577 const val MANIFEST_NOTIFICATION_INFO_KEY = "notification" 578 const val MANIFEST_NOTIFICATION_ICON_URL_KEY = "iconUrl" 579 const val MANIFEST_NOTIFICATION_COLOR_KEY = "color" 580 const val MANIFEST_NOTIFICATION_ANDROID_MODE = "androidMode" 581 const val MANIFEST_NOTIFICATION_ANDROID_COLLAPSED_TITLE = "androidCollapsedTitle" 582 583 // Debugging 584 const val MANIFEST_DEBUGGER_HOST_KEY = "debuggerHost" 585 const val MANIFEST_MAIN_MODULE_NAME_KEY = "mainModuleName" 586 587 // Splash 588 const val MANIFEST_SPLASH_INFO_KEY = "splash" 589 const val MANIFEST_SPLASH_IMAGE_URL_KEY = "imageUrl" 590 const val MANIFEST_SPLASH_RESIZE_MODE_KEY = "resizeMode" 591 const val MANIFEST_SPLASH_BACKGROUND_COLOR_KEY = "backgroundColor" 592 593 // Updates 594 const val MANIFEST_UPDATES_INFO_KEY = "updates" 595 const val MANIFEST_UPDATES_TIMEOUT_KEY = "fallbackToCacheTimeout" 596 const val MANIFEST_UPDATES_CHECK_AUTOMATICALLY_KEY = "checkAutomatically" 597 const val MANIFEST_UPDATES_CHECK_AUTOMATICALLY_ON_LOAD = "ON_LOAD" 598 const val MANIFEST_UPDATES_CHECK_AUTOMATICALLY_ON_ERROR = "ON_ERROR_RECOVERY" 599 600 // Development client 601 const val MANIFEST_DEVELOPMENT_CLIENT_KEY = "developmentClient" 602 const val MANIFEST_DEVELOPMENT_CLIENT_SILENT_LAUNCH_KEY = "silentLaunch" 603 const val DEEP_LINK_SEPARATOR = "--" 604 const val DEEP_LINK_SEPARATOR_WITH_SLASH = "--/" 605 const val QUERY_PARAM_KEY_RELEASE_CHANNEL = "release-channel" 606 const val QUERY_PARAM_KEY_EXPO_UPDATES_RUNTIME_VERSION = "runtime-version" 607 const val QUERY_PARAM_KEY_EXPO_UPDATES_CHANNEL_NAME = "channel-name" 608 609 private const val MAX_BITMAP_SIZE = 192 610 private const val REDIRECT_SNIPPET = "exp.host/--/to-exp/" 611 private const val ANONYMOUS_EXPERIENCE_PREFIX = "@anonymous/" 612 private const val EMBEDDED_KERNEL_MANIFEST_ASSET = "kernel-manifest.json" 613 private const val EXPONENT_SERVER_HEADER = "Exponent-Server" 614 615 private var hasShownKernelManifestLog = false 616 617 @Throws(JSONException::class) 618 fun normalizeRawManifestInPlace(rawManifest: RawManifest, manifestUrl: String) { 619 rawManifest.mutateInternalJSONInPlace(object : InternalJSONMutator { 620 override fun updateJSON(json: JSONObject) { 621 if (!json.has(MANIFEST_ID_KEY)) { 622 json.put(MANIFEST_ID_KEY, manifestUrl) 623 } 624 if (!json.has(MANIFEST_NAME_KEY)) { 625 json.put(MANIFEST_NAME_KEY, "My New Experience") 626 } 627 if (!json.has(MANIFEST_PRIMARY_COLOR_KEY)) { 628 json.put(MANIFEST_PRIMARY_COLOR_KEY, "#023C69") 629 } 630 if (!json.has(MANIFEST_ICON_URL_KEY)) { 631 json.put( 632 MANIFEST_ICON_URL_KEY, 633 "https://d3lwq5rlu14cro.cloudfront.net/ExponentEmptyManifest_192.png" 634 ) 635 } 636 if (!json.has(MANIFEST_ORIENTATION_KEY)) { 637 json.put(MANIFEST_ORIENTATION_KEY, "default") 638 } 639 } 640 }) 641 } 642 } 643 644 init { 645 val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt() 646 // Use 1/16th of the available memory for this memory cache. 647 val cacheSize = maxMemory / 16 648 memoryCache = object : LruCache<String, Bitmap>(cacheSize) { 649 override fun sizeOf(key: String?, bitmap: Bitmap): Int { 650 return bitmap.byteCount / 1024 651 } 652 } 653 } 654 } 655