1 // Copyright 2015-present 650 Industries. All rights reserved. 2 package host.exp.exponent.kernel 3 4 import android.app.Activity 5 import android.app.ActivityManager 6 import android.app.ActivityManager.AppTask 7 import android.app.ActivityManager.RecentTaskInfo 8 import android.app.Application 9 import android.app.RemoteInput 10 import android.content.Context 11 import android.content.Intent 12 import android.net.Uri 13 import android.nfc.NfcAdapter 14 import android.os.Bundle 15 import android.util.Log 16 import android.widget.Toast 17 import com.facebook.hermes.reactexecutor.HermesExecutorFactory 18 import com.facebook.proguard.annotations.DoNotStrip 19 import com.facebook.react.ReactInstanceManager 20 import com.facebook.react.ReactRootView 21 import com.facebook.react.bridge.Arguments 22 import com.facebook.react.bridge.JavaScriptExecutorFactory 23 import com.facebook.react.bridge.ReadableMap 24 import com.facebook.react.common.LifecycleState 25 import com.facebook.react.jscexecutor.JSCExecutorFactory 26 import com.facebook.react.modules.network.ReactCookieJarContainer 27 import com.facebook.react.modules.systeminfo.AndroidInfoHelpers 28 import com.facebook.react.shell.MainReactPackage 29 import com.facebook.soloader.SoLoader 30 import de.greenrobot.event.EventBus 31 import expo.modules.jsonutils.require 32 import expo.modules.notifications.service.NotificationsService.Companion.getNotificationResponseFromOpenIntent 33 import expo.modules.notifications.service.delegates.ExpoHandlingDelegate 34 import expo.modules.manifests.core.Manifest 35 import expo.modules.manifests.core.NewManifest 36 import host.exp.exponent.* 37 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderCallback 38 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderStatus 39 import host.exp.exponent.analytics.EXL 40 import host.exp.exponent.di.NativeModuleDepsProvider 41 import host.exp.exponent.exceptions.ExceptionUtils 42 import host.exp.exponent.experience.BaseExperienceActivity 43 import host.exp.exponent.experience.ErrorActivity 44 import host.exp.exponent.experience.ExperienceActivity 45 import host.exp.exponent.experience.HomeActivity 46 import host.exp.exponent.headless.InternalHeadlessAppLoader 47 import host.exp.exponent.kernel.ExponentErrorMessage.Companion.developerErrorMessage 48 import host.exp.exponent.kernel.ExponentKernelModuleProvider.KernelEventCallback 49 import host.exp.exponent.kernel.ExponentKernelModuleProvider.queueEvent 50 import host.exp.exponent.kernel.ExponentUrls.toHttp 51 import host.exp.exponent.kernel.KernelConstants.ExperienceOptions 52 import host.exp.exponent.network.ExponentNetwork 53 import host.exp.exponent.notifications.ExponentNotification 54 import host.exp.exponent.notifications.ExponentNotificationManager 55 import host.exp.exponent.notifications.NotificationActionCenter 56 import host.exp.exponent.notifications.ScopedNotificationsUtils 57 import host.exp.exponent.storage.ExponentDB 58 import host.exp.exponent.storage.ExponentSharedPreferences 59 import host.exp.exponent.utils.AsyncCondition 60 import host.exp.exponent.utils.AsyncCondition.AsyncConditionListener 61 import host.exp.exponent.utils.BundleJSONConverter 62 import host.exp.expoview.BuildConfig 63 import host.exp.expoview.ExpoViewBuildConfig 64 import host.exp.expoview.Exponent 65 import host.exp.expoview.Exponent.BundleListener 66 import okhttp3.OkHttpClient 67 import org.json.JSONException 68 import org.json.JSONObject 69 import versioned.host.exp.exponent.ExpoTurboPackage 70 import versioned.host.exp.exponent.ExponentPackage 71 import versioned.host.exp.exponent.ReactUnthemedRootView 72 import java.lang.ref.WeakReference 73 import java.util.* 74 import java.util.concurrent.TimeUnit 75 import javax.inject.Inject 76 77 // TOOD: need to figure out when we should reload the kernel js. Do we do it every time you visit 78 // the home screen? only when the app gets kicked out of memory? 79 class Kernel : KernelInterface() { 80 class KernelStartedRunningEvent 81 82 class ExperienceActivityTask(val manifestUrl: String) { 83 var taskId = 0 84 var experienceActivity: WeakReference<ExperienceActivity>? = null 85 var activityId = 0 86 var bundleUrl: String? = null 87 } 88 89 // React 90 var reactInstanceManager: ReactInstanceManager? = null 91 private set 92 93 // Contexts 94 @Inject 95 lateinit var context: Context 96 97 @Inject 98 lateinit var applicationContext: Application 99 100 @Inject 101 lateinit var exponentManifest: ExponentManifest 102 103 @Inject 104 lateinit var exponentSharedPreferences: ExponentSharedPreferences 105 106 @Inject 107 lateinit var exponentNetwork: ExponentNetwork 108 109 var activityContext: Activity? = null 110 set(value) { 111 if (value != null) { 112 field = value 113 } 114 } 115 116 private var optimisticActivity: ExperienceActivity? = null 117 118 private var optimisticTaskId: Int? = null 119 120 private fun experienceActivityTaskForTaskId(taskId: Int): ExperienceActivityTask? { 121 return manifestUrlToExperienceActivityTask.values.find { it.taskId == taskId } 122 } 123 124 // Misc 125 var isStarted = false 126 private set 127 private var hasError = false 128 129 private fun updateKernelRNOkHttp() { 130 val client = OkHttpClient.Builder() 131 .connectTimeout(0, TimeUnit.MILLISECONDS) 132 .readTimeout(0, TimeUnit.MILLISECONDS) 133 .writeTimeout(0, TimeUnit.MILLISECONDS) 134 .cookieJar(ReactCookieJarContainer()) 135 .cache(exponentNetwork.cache) 136 137 if (BuildConfig.DEBUG) { 138 // FIXME: 8/9/17 139 // broke with lib versioning 140 // clientBuilder.addNetworkInterceptor(new StethoInterceptor()); 141 } 142 ReactNativeStaticHelpers.setExponentNetwork(exponentNetwork) 143 } 144 145 private val kernelInitialURL: String? 146 get() { 147 val activity = activityContext ?: return null 148 val intent = activity.intent ?: return null 149 val action = intent.action 150 val uri = intent.data 151 return if (( 152 uri != null && 153 ((Intent.ACTION_VIEW == action) || (NfcAdapter.ACTION_NDEF_DISCOVERED == action)) 154 ) 155 ) { 156 uri.toString() 157 } else null 158 } 159 160 // Don't call this until a loading screen is up, since it has to do some work on the main thread. 161 fun startJSKernel(activity: Activity?) { 162 if (Constants.isStandaloneApp()) { 163 return 164 } 165 activityContext = activity 166 SoLoader.init(context, false) 167 synchronized(this) { 168 if (isStarted && !hasError) { 169 return 170 } 171 isStarted = true 172 } 173 hasError = false 174 if (!exponentSharedPreferences.shouldUseEmbeddedKernel()) { 175 try { 176 // Make sure we can get the manifest successfully. This can fail in dev mode 177 // if the kernel packager is not running. 178 exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest 179 } catch (e: Throwable) { 180 Exponent.instance 181 .runOnUiThread { // Hack to make this show up for a while. Can't use an Alert because LauncherActivity has a transparent theme. This should only be seen by internal developers. 182 var i = 0 183 while (i < 3) { 184 Toast.makeText( 185 activityContext, 186 "Kernel manifest invalid. Make sure `expo start` is running inside of exponent/home and rebuild the app.", 187 Toast.LENGTH_LONG 188 ).show() 189 i++ 190 } 191 } 192 return 193 } 194 } 195 196 // On first run use the embedded kernel js but fire off a request for the new js in the background. 197 val bundleUrlToLoad = 198 bundleUrl + (if (ExpoViewBuildConfig.DEBUG) "" else "?versionName=" + ExpoViewKernel.instance.versionName) 199 if (exponentSharedPreferences.shouldUseEmbeddedKernel()) { 200 kernelBundleListener().onBundleLoaded(Constants.EMBEDDED_KERNEL_PATH) 201 } else { 202 var shouldNotUseKernelCache = 203 exponentSharedPreferences.getBoolean(ExponentSharedPreferences.ExponentSharedPreferencesKey.SHOULD_NOT_USE_KERNEL_CACHE) 204 if (!ExpoViewBuildConfig.DEBUG) { 205 val oldKernelRevisionId = 206 exponentSharedPreferences.getString(ExponentSharedPreferences.ExponentSharedPreferencesKey.KERNEL_REVISION_ID, "") 207 if (oldKernelRevisionId != kernelRevisionId) { 208 shouldNotUseKernelCache = true 209 } 210 } 211 Exponent.instance.loadJSBundle( 212 null, 213 bundleUrlToLoad, 214 bundleAssetRequestHeaders, 215 KernelConstants.KERNEL_BUNDLE_ID, 216 RNObject.UNVERSIONED, 217 kernelBundleListener(), 218 shouldNotUseKernelCache 219 ) 220 } 221 } 222 223 private fun kernelBundleListener(): BundleListener { 224 return object : BundleListener { 225 override fun onBundleLoaded(localBundlePath: String) { 226 if (!ExpoViewBuildConfig.DEBUG) { 227 exponentSharedPreferences.setString( 228 ExponentSharedPreferences.ExponentSharedPreferencesKey.KERNEL_REVISION_ID, 229 kernelRevisionId 230 ) 231 } 232 Exponent.instance.runOnUiThread { 233 val initialURL = kernelInitialURL 234 val builder = ReactInstanceManager.builder() 235 .setApplication(applicationContext) 236 .setCurrentActivity(activityContext) 237 .setJSBundleFile(localBundlePath) 238 .setJavaScriptExecutorFactory(jsExecutorFactory) 239 .addPackage(MainReactPackage()) 240 .addPackage( 241 ExponentPackage.kernelExponentPackage( 242 context, 243 exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest, 244 HomeActivity.homeExpoPackages(), 245 HomeActivity.Companion, 246 initialURL 247 ) 248 ) 249 .addPackage( 250 ExpoTurboPackage.kernelExpoTurboPackage( 251 exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest, initialURL 252 ) 253 ) 254 .setInitialLifecycleState(LifecycleState.RESUMED) 255 if (!KernelConfig.FORCE_NO_KERNEL_DEBUG_MODE && exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest.isDevelopmentMode()) { 256 Exponent.enableDeveloperSupport( 257 kernelDebuggerHost, kernelMainModuleName, 258 RNObject.wrap(builder) 259 ) 260 } 261 reactInstanceManager = builder.build() 262 reactInstanceManager!!.createReactContextInBackground() 263 reactInstanceManager!!.onHostResume(activityContext, null) 264 isRunning = true 265 EventBus.getDefault().postSticky(KernelStartedRunningEvent()) 266 EXL.d(TAG, "Kernel started running.") 267 268 // Reset this flag if we crashed 269 exponentSharedPreferences.setBoolean( 270 ExponentSharedPreferences.ExponentSharedPreferencesKey.SHOULD_NOT_USE_KERNEL_CACHE, 271 false 272 ) 273 } 274 } 275 276 override fun onError(e: Exception) { 277 setHasError() 278 if (ExpoViewBuildConfig.DEBUG) { 279 handleError("Can't load kernel. Are you sure your packager is running and your phone is on the same wifi? " + e.message) 280 } else { 281 handleError("Expo requires an internet connection.") 282 EXL.d(TAG, "Expo requires an internet connection." + e.message) 283 } 284 } 285 } 286 } 287 288 private val kernelDebuggerHost: String 289 get() = exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest.getDebuggerHost() 290 private val kernelMainModuleName: String 291 get() = exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest.getMainModuleName() 292 private val bundleUrl: String? 293 get() { 294 return try { 295 exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest.getBundleURL() 296 } catch (e: JSONException) { 297 KernelProvider.instance.handleError(e) 298 null 299 } 300 } 301 private val bundleAssetRequestHeaders: JSONObject 302 get() { 303 return try { 304 val manifestAndAssetRequestHeaders = exponentManifest.getKernelManifestAndAssetRequestHeaders() 305 val manifest = manifestAndAssetRequestHeaders.manifest 306 if (manifest is NewManifest) { 307 val bundleKey = manifest.getLaunchAsset().getString("key") 308 val map: Map<String, JSONObject> = manifestAndAssetRequestHeaders.assetRequestHeaders.let { it.keys().asSequence().associateWith { key -> it.require(key) } } ?: mapOf() 309 map[bundleKey] ?: JSONObject() 310 } else { 311 JSONObject() 312 } 313 } catch (e: JSONException) { 314 KernelProvider.instance.handleError(e) 315 JSONObject() 316 } 317 } 318 private val kernelRevisionId: String? 319 get() { 320 return try { 321 exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest.getRevisionId() 322 } catch (e: JSONException) { 323 KernelProvider.instance.handleError(e) 324 null 325 } 326 } 327 var isRunning: Boolean = false 328 get() = field && !hasError 329 private set 330 331 val reactRootView: ReactRootView 332 get() { 333 val reactRootView: ReactRootView = ReactUnthemedRootView(activityContext) 334 reactRootView.startReactApplication( 335 reactInstanceManager, 336 KernelConstants.HOME_MODULE_NAME, 337 kernelLaunchOptions 338 ) 339 return reactRootView 340 } 341 private val kernelLaunchOptions: Bundle 342 get() { 343 val exponentProps = JSONObject() 344 val referrer = exponentSharedPreferences.getString(ExponentSharedPreferences.ExponentSharedPreferencesKey.REFERRER_KEY) 345 if (referrer != null) { 346 try { 347 exponentProps.put("referrer", referrer) 348 } catch (e: JSONException) { 349 EXL.e(TAG, e) 350 } 351 } 352 val bundle = Bundle() 353 try { 354 bundle.putBundle("exp", BundleJSONConverter.convertToBundle(exponentProps)) 355 } catch (e: JSONException) { 356 throw Error("JSONObject failed to be converted to Bundle", e) 357 } 358 return bundle 359 } 360 private val jsExecutorFactory: JavaScriptExecutorFactory 361 get() { 362 val manifest = exponentManifest.getKernelManifestAndAssetRequestHeaders().manifest 363 val appName = manifest.getName() ?: "" 364 val deviceName = AndroidInfoHelpers.getFriendlyDeviceName() 365 366 val jsEngineFromManifest = manifest.jsEngine 367 return if (jsEngineFromManifest == "hermes") HermesExecutorFactory() else JSCExecutorFactory( 368 appName, 369 deviceName 370 ) 371 } 372 373 fun hasOptionsForManifestUrl(manifestUrl: String?): Boolean { 374 return manifestUrlToOptions.containsKey(manifestUrl) 375 } 376 377 fun popOptionsForManifestUrl(manifestUrl: String?): ExperienceOptions? { 378 return manifestUrlToOptions.remove(manifestUrl) 379 } 380 381 fun addAppLoaderForManifestUrl(manifestUrl: String, appLoader: ExpoUpdatesAppLoader) { 382 manifestUrlToAppLoader[manifestUrl] = appLoader 383 } 384 385 override fun getAppLoaderForManifestUrl(manifestUrl: String?): ExpoUpdatesAppLoader? { 386 return manifestUrlToAppLoader[manifestUrl] 387 } 388 389 fun getExperienceActivityTask(manifestUrl: String): ExperienceActivityTask { 390 var task = manifestUrlToExperienceActivityTask[manifestUrl] 391 if (task != null) { 392 return task 393 } 394 task = ExperienceActivityTask(manifestUrl) 395 manifestUrlToExperienceActivityTask[manifestUrl] = task 396 return task 397 } 398 399 fun removeExperienceActivityTask(manifestUrl: String?) { 400 if (manifestUrl != null) { 401 manifestUrlToExperienceActivityTask.remove(manifestUrl) 402 } 403 } 404 405 fun openHomeActivity() { 406 val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 407 for (task: AppTask in manager.appTasks) { 408 val baseIntent = task.taskInfo.baseIntent 409 if ((HomeActivity::class.java.name == baseIntent.component!!.className)) { 410 task.moveToFront() 411 return 412 } 413 } 414 val intent = Intent(activityContext, HomeActivity::class.java) 415 addIntentDocumentFlags(intent) 416 activityContext!!.startActivity(intent) 417 } 418 419 private fun openShellAppActivity(forceCache: Boolean) { 420 try { 421 val activityClass = Class.forName("host.exp.exponent.MainActivity") 422 val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 423 for (task: AppTask in manager.appTasks) { 424 val baseIntent = task.taskInfo.baseIntent 425 if ((activityClass.name == baseIntent.component!!.className)) { 426 moveTaskToFront(task.taskInfo.id) 427 return 428 } 429 } 430 val intent = Intent(activityContext, activityClass) 431 addIntentDocumentFlags(intent) 432 if (forceCache) { 433 intent.putExtra(KernelConstants.LOAD_FROM_CACHE_KEY, true) 434 } 435 activityContext!!.startActivity(intent) 436 } catch (e: ClassNotFoundException) { 437 throw IllegalStateException("Could not find activity to open (MainActivity is not present).") 438 } 439 } 440 441 /* 442 * 443 * Manifests 444 * 445 */ 446 fun handleIntent(activity: Activity, intent: Intent) { 447 try { 448 if (intent.getBooleanExtra("EXKernelDisableNuxDefaultsKey", false)) { 449 Constants.DISABLE_NUX = true 450 } 451 } catch (e: Throwable) { 452 } 453 activityContext = activity 454 if (intent.action != null && (ExpoHandlingDelegate.OPEN_APP_INTENT_ACTION == intent.action)) { 455 if (!openExperienceFromNotificationIntent(intent)) { 456 openDefaultUrl() 457 } 458 return 459 } 460 val bundle = intent.extras 461 val uri = intent.data 462 val intentUri = uri?.toString() 463 if (bundle != null) { 464 // Notification 465 val notification = bundle.getString(KernelConstants.NOTIFICATION_KEY) // deprecated 466 val notificationObject = bundle.getString(KernelConstants.NOTIFICATION_OBJECT_KEY) 467 val notificationManifestUrl = bundle.getString(KernelConstants.NOTIFICATION_MANIFEST_URL_KEY) 468 if (notificationManifestUrl != null) { 469 val exponentNotification = ExponentNotification.fromJSONObjectString(notificationObject) 470 if (exponentNotification != null) { 471 // Add action type 472 if (bundle.containsKey(KernelConstants.NOTIFICATION_ACTION_TYPE_KEY)) { 473 exponentNotification.actionType = bundle.getString(KernelConstants.NOTIFICATION_ACTION_TYPE_KEY) 474 val manager = ExponentNotificationManager(context) 475 val experienceKey = ExperienceKey(exponentNotification.experienceScopeKey) 476 manager.cancel(experienceKey, exponentNotification.notificationId) 477 } 478 // Add remote input 479 val remoteInput = RemoteInput.getResultsFromIntent(intent) 480 if (remoteInput != null) { 481 exponentNotification.inputText = remoteInput.getString(NotificationActionCenter.KEY_TEXT_REPLY) 482 } 483 } 484 openExperience( 485 ExperienceOptions( 486 notificationManifestUrl, 487 intentUri ?: notificationManifestUrl, 488 notification, 489 exponentNotification 490 ) 491 ) 492 return 493 } 494 495 // Shortcut 496 // TODO: Remove once we decide to stop supporting shortcuts to experiences. 497 val shortcutManifestUrl = bundle.getString(KernelConstants.SHORTCUT_MANIFEST_URL_KEY) 498 if (shortcutManifestUrl != null) { 499 openExperience(ExperienceOptions(shortcutManifestUrl, intentUri, null)) 500 return 501 } 502 } 503 if (uri != null && shouldOpenUrl(uri)) { 504 if (Constants.INITIAL_URL == null) { 505 // We got an "exp://", "exps://", "http://", or "https://" app link 506 openExperience(ExperienceOptions(uri.toString(), uri.toString(), null)) 507 return 508 } else { 509 // We got a custom scheme link 510 // TODO: we still might want to parse this if we're running a different experience inside a 511 // shell app. For example, we are running Brighten in the List shell and go to Twitter login. 512 // We might want to set the return uri to thelistapp://exp.host/@brighten/brighten+deeplink 513 // But we also can't break thelistapp:// deep links that look like thelistapp://l/listid 514 openExperience(ExperienceOptions(Constants.INITIAL_URL, uri.toString(), null)) 515 return 516 } 517 } 518 openDefaultUrl() 519 } 520 521 // Certain links (i.e. 'expo.io/expo-go') should just open the HomeScreen 522 private fun shouldOpenUrl(uri: Uri): Boolean { 523 val host = uri.host ?: "" 524 val path = uri.path ?: "" 525 return !(((host == "expo.io") || (host == "expo.dev")) && (path == "/expo-go")) 526 } 527 528 private fun openExperienceFromNotificationIntent(intent: Intent): Boolean { 529 val response = getNotificationResponseFromOpenIntent(intent) 530 val experienceScopeKey = ScopedNotificationsUtils.getExperienceScopeKey(response) ?: return false 531 val exponentDBObject = try { 532 val exponentDBObjectInner = ExponentDB.experienceScopeKeyToExperienceSync(experienceScopeKey) 533 if (exponentDBObjectInner == null) { 534 Log.w("expo-notifications", "Couldn't find experience from scopeKey: $experienceScopeKey") 535 } 536 exponentDBObjectInner 537 } catch (e: JSONException) { 538 Log.w("expo-notifications", "Couldn't deserialize experience from scopeKey: $experienceScopeKey") 539 null 540 } ?: return false 541 542 val manifestUrl = exponentDBObject.manifestUrl 543 openExperience(ExperienceOptions(manifestUrl, manifestUrl, null)) 544 return true 545 } 546 547 private fun openDefaultUrl() { 548 val defaultUrl = 549 if (Constants.INITIAL_URL == null) KernelConstants.HOME_MANIFEST_URL else Constants.INITIAL_URL 550 openExperience(ExperienceOptions(defaultUrl, defaultUrl, null)) 551 } 552 553 override fun openExperience(options: ExperienceOptions) { 554 openManifestUrl(getManifestUrlFromFullUri(options.manifestUri), options, true) 555 } 556 557 private fun getManifestUrlFromFullUri(uriString: String?): String? { 558 if (uriString == null) { 559 return null 560 } 561 562 val uri = Uri.parse(uriString) 563 val builder = uri.buildUpon() 564 val deepLinkPositionDashes = 565 uriString.indexOf(ExponentManifest.DEEP_LINK_SEPARATOR_WITH_SLASH) 566 if (deepLinkPositionDashes >= 0) { 567 // do this safely so we preserve any query string 568 val pathSegments = uri.pathSegments 569 builder.path(null) 570 for (segment: String in pathSegments) { 571 if ((ExponentManifest.DEEP_LINK_SEPARATOR == segment)) { 572 break 573 } 574 builder.appendEncodedPath(segment) 575 } 576 } 577 578 // transfer the release-channel param to the built URL as this will cause Expo Go to treat 579 // this as a different project 580 var releaseChannel = uri.getQueryParameter(ExponentManifest.QUERY_PARAM_KEY_RELEASE_CHANNEL) 581 builder.query(null) 582 if (releaseChannel != null) { 583 // release channels cannot contain the ' ' character, so if this is present, 584 // it must be an encoded form of '+' which indicated a deep link in SDK <27. 585 // therefore, nothing after this is part of the release channel name so we should strip it. 586 // TODO: remove this check once SDK 26 and below are no longer supported 587 val releaseChannelDeepLinkPosition = releaseChannel.indexOf(' ') 588 if (releaseChannelDeepLinkPosition > -1) { 589 releaseChannel = releaseChannel.substring(0, releaseChannelDeepLinkPosition) 590 } 591 builder.appendQueryParameter( 592 ExponentManifest.QUERY_PARAM_KEY_RELEASE_CHANNEL, 593 releaseChannel 594 ) 595 } 596 597 // transfer the expo-updates query params: runtime-version, channel-name 598 val expoUpdatesQueryParameters = listOf( 599 ExponentManifest.QUERY_PARAM_KEY_EXPO_UPDATES_RUNTIME_VERSION, 600 ExponentManifest.QUERY_PARAM_KEY_EXPO_UPDATES_CHANNEL_NAME 601 ) 602 for (queryParameter: String in expoUpdatesQueryParameters) { 603 val queryParameterValue = uri.getQueryParameter(queryParameter) 604 if (queryParameterValue != null) { 605 builder.appendQueryParameter(queryParameter, queryParameterValue) 606 } 607 } 608 609 // ignore fragments as well (e.g. those added by auth-session) 610 builder.fragment(null) 611 var newUriString = builder.build().toString() 612 val deepLinkPositionPlus = newUriString.indexOf('+') 613 if (deepLinkPositionPlus >= 0 && deepLinkPositionDashes < 0) { 614 // need to keep this for backwards compatibility 615 newUriString = newUriString.substring(0, deepLinkPositionPlus) 616 } 617 618 // manifest url doesn't have a trailing slash 619 if (newUriString.isNotEmpty()) { 620 val lastUrlChar = newUriString[newUriString.length - 1] 621 if (lastUrlChar == '/') { 622 newUriString = newUriString.substring(0, newUriString.length - 1) 623 } 624 } 625 return newUriString 626 } 627 628 private fun openManifestUrl( 629 manifestUrl: String?, 630 options: ExperienceOptions?, 631 isOptimistic: Boolean, 632 forceCache: Boolean = false 633 ) { 634 SoLoader.init(context, false) 635 if (options == null) { 636 manifestUrlToOptions.remove(manifestUrl) 637 } else { 638 manifestUrlToOptions[manifestUrl] = options 639 } 640 if (manifestUrl == null || (manifestUrl == KernelConstants.HOME_MANIFEST_URL)) { 641 openHomeActivity() 642 return 643 } 644 if (Constants.isStandaloneApp()) { 645 openShellAppActivity(forceCache) 646 return 647 } 648 ErrorActivity.clearErrorList() 649 val tasks: List<AppTask> = experienceActivityTasks 650 var existingTask: AppTask? = run { 651 for (i in tasks.indices) { 652 val task = tasks[i] 653 // When deep linking from `NotificationForwarderActivity`, the task will finish immediately. 654 // There is race condition to retrieve the taskInfo from the finishing task. 655 // Uses try-catch to handle the cases. 656 try { 657 val baseIntent = task.taskInfo.baseIntent 658 if (baseIntent.hasExtra(KernelConstants.MANIFEST_URL_KEY) && ( 659 baseIntent.getStringExtra( 660 KernelConstants.MANIFEST_URL_KEY 661 ) == manifestUrl 662 ) 663 ) { 664 return@run task 665 } 666 } catch (e: Exception) {} 667 } 668 return@run null 669 } 670 671 if (isOptimistic && existingTask == null) { 672 openOptimisticExperienceActivity(manifestUrl) 673 } 674 if (existingTask != null) { 675 try { 676 moveTaskToFront(existingTask.taskInfo.id) 677 } catch (e: IllegalArgumentException) { 678 // Sometimes task can't be found. 679 existingTask = null 680 openOptimisticExperienceActivity(manifestUrl) 681 } 682 } 683 val finalExistingTask = existingTask 684 if (existingTask == null) { 685 ExpoUpdatesAppLoader( 686 manifestUrl, 687 object : AppLoaderCallback { 688 override fun onOptimisticManifest(optimisticManifest: Manifest) { 689 Exponent.instance 690 .runOnUiThread { sendOptimisticManifestToExperienceActivity(optimisticManifest) } 691 } 692 693 override fun onManifestCompleted(manifest: Manifest) { 694 Exponent.instance.runOnUiThread { 695 try { 696 openManifestUrlStep2(manifestUrl, manifest, finalExistingTask) 697 } catch (e: JSONException) { 698 handleError(e) 699 } 700 } 701 } 702 703 override fun onBundleCompleted(localBundlePath: String) { 704 Exponent.instance.runOnUiThread { sendBundleToExperienceActivity(localBundlePath) } 705 } 706 707 override fun emitEvent(params: JSONObject) { 708 val task = manifestUrlToExperienceActivityTask[manifestUrl] 709 if (task != null) { 710 val experienceActivity = task.experienceActivity!!.get() 711 experienceActivity?.emitUpdatesEvent(params) 712 } 713 } 714 715 override fun updateStatus(status: AppLoaderStatus) { 716 if (optimisticActivity != null) { 717 optimisticActivity!!.setLoadingProgressStatusIfEnabled(status) 718 } 719 } 720 721 override fun onError(e: Exception) { 722 Exponent.instance.runOnUiThread { handleError(e) } 723 } 724 }, 725 forceCache 726 ).start(context) 727 } 728 } 729 730 @Throws(JSONException::class) 731 private fun openManifestUrlStep2( 732 manifestUrl: String, 733 manifest: Manifest, 734 existingTask: AppTask? 735 ) { 736 val bundleUrl = toHttp(manifest.getBundleURL()) 737 val task = getExperienceActivityTask(manifestUrl) 738 task.bundleUrl = bundleUrl 739 ExponentManifest.normalizeManifestInPlace(manifest, manifestUrl) 740 if (existingTask == null) { 741 sendManifestToExperienceActivity(manifestUrl, manifest, bundleUrl) 742 } 743 val params = Arguments.createMap().apply { 744 putString("manifestUrl", manifestUrl) 745 putString("manifestString", manifest.toString()) 746 } 747 queueEvent( 748 "ExponentKernel.addHistoryItem", params, 749 object : KernelEventCallback { 750 override fun onEventSuccess(result: ReadableMap) { 751 EXL.d(TAG, "Successfully called ExponentKernel.addHistoryItem in kernel JS.") 752 } 753 754 override fun onEventFailure(errorMessage: String?) { 755 EXL.e(TAG, "Error calling ExponentKernel.addHistoryItem in kernel JS: $errorMessage") 756 } 757 } 758 ) 759 killOrphanedLauncherActivities() 760 } 761 762 /* 763 * 764 * Optimistic experiences 765 * 766 */ 767 private fun openOptimisticExperienceActivity(manifestUrl: String?) { 768 try { 769 val intent = Intent(activityContext, ExperienceActivity::class.java).apply { 770 addIntentDocumentFlags(this) 771 putExtra(KernelConstants.MANIFEST_URL_KEY, manifestUrl) 772 putExtra(KernelConstants.IS_OPTIMISTIC_KEY, true) 773 } 774 activityContext!!.startActivity(intent) 775 } catch (e: Throwable) { 776 EXL.e(TAG, e) 777 } 778 } 779 780 fun setOptimisticActivity(experienceActivity: ExperienceActivity, taskId: Int) { 781 optimisticActivity = experienceActivity 782 optimisticTaskId = taskId 783 AsyncCondition.notify(KernelConstants.OPEN_OPTIMISTIC_EXPERIENCE_ACTIVITY_KEY) 784 AsyncCondition.notify(KernelConstants.OPEN_EXPERIENCE_ACTIVITY_KEY) 785 } 786 787 fun sendOptimisticManifestToExperienceActivity(optimisticManifest: Manifest) { 788 AsyncCondition.wait( 789 KernelConstants.OPEN_OPTIMISTIC_EXPERIENCE_ACTIVITY_KEY, 790 object : AsyncConditionListener { 791 override fun isReady(): Boolean { 792 return optimisticActivity != null && optimisticTaskId != null 793 } 794 795 override fun execute() { 796 optimisticActivity!!.setOptimisticManifest(optimisticManifest) 797 } 798 } 799 ) 800 } 801 802 private fun sendManifestToExperienceActivity( 803 manifestUrl: String, 804 manifest: Manifest, 805 bundleUrl: String, 806 ) { 807 AsyncCondition.wait( 808 KernelConstants.OPEN_EXPERIENCE_ACTIVITY_KEY, 809 object : AsyncConditionListener { 810 override fun isReady(): Boolean { 811 return optimisticActivity != null && optimisticTaskId != null 812 } 813 814 override fun execute() { 815 optimisticActivity!!.setManifest(manifestUrl, manifest, bundleUrl) 816 AsyncCondition.notify(KernelConstants.LOAD_BUNDLE_FOR_EXPERIENCE_ACTIVITY_KEY) 817 } 818 } 819 ) 820 } 821 822 private fun sendBundleToExperienceActivity(localBundlePath: String) { 823 AsyncCondition.wait( 824 KernelConstants.LOAD_BUNDLE_FOR_EXPERIENCE_ACTIVITY_KEY, 825 object : AsyncConditionListener { 826 override fun isReady(): Boolean { 827 return optimisticActivity != null && optimisticTaskId != null 828 } 829 830 override fun execute() { 831 optimisticActivity!!.setBundle(localBundlePath) 832 optimisticActivity = null 833 optimisticTaskId = null 834 } 835 } 836 ) 837 } 838 839 /* 840 * 841 * Tasks 842 * 843 */ 844 val tasks: List<AppTask> 845 get() { 846 val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 847 return manager.appTasks 848 } 849 850 // Get list of tasks in our format. 851 val experienceActivityTasks: List<AppTask> 852 get() = tasks 853 854 // Sometimes LauncherActivity.finish() doesn't close the activity and task. Not sure why exactly. 855 // Thought it was related to launchMode="singleTask" but other launchModes seem to have the same problem. 856 // This can be reproduced by creating a shortcut, exiting app, clicking on shortcut, refreshing, pressing 857 // home, clicking on shortcut, click recent apps button. There will be a blank LauncherActivity behind 858 // the ExperienceActivity. killOrphanedLauncherActivities solves this but would be nice to figure out 859 // the root cause. 860 private fun killOrphanedLauncherActivities() { 861 try { 862 // Crash with NoSuchFieldException instead of hard crashing at taskInfo.numActivities 863 RecentTaskInfo::class.java.getDeclaredField("numActivities") 864 for (task: AppTask in tasks) { 865 val taskInfo = task.taskInfo 866 if (taskInfo.numActivities == 0 && (taskInfo.baseIntent.action == Intent.ACTION_MAIN)) { 867 task.finishAndRemoveTask() 868 return 869 } 870 if (taskInfo.numActivities == 1 && (taskInfo.topActivity!!.className == LauncherActivity::class.java.name)) { 871 task.finishAndRemoveTask() 872 return 873 } 874 } 875 } catch (e: NoSuchFieldException) { 876 // Don't EXL here because this isn't actually a problem 877 Log.e(TAG, e.toString()) 878 } catch (e: Throwable) { 879 EXL.e(TAG, e) 880 } 881 } 882 883 fun moveTaskToFront(taskId: Int) { 884 tasks.find { it.taskInfo.id == taskId }?.also { task -> 885 // If we have the task in memory, tell the ExperienceActivity to check for new options. 886 // Otherwise options will be added in initialProps when the Experience starts. 887 val exponentTask = experienceActivityTaskForTaskId(taskId) 888 if (exponentTask != null) { 889 val experienceActivity = exponentTask.experienceActivity!!.get() 890 experienceActivity?.shouldCheckOptions() 891 } 892 task.moveToFront() 893 } 894 } 895 896 fun killActivityStack(activity: Activity) { 897 val exponentTask = experienceActivityTaskForTaskId(activity.taskId) 898 if (exponentTask != null) { 899 removeExperienceActivityTask(exponentTask.manifestUrl) 900 } 901 902 // Kill the current task. 903 val manager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 904 manager.appTasks.find { it.taskInfo.id == activity.taskId }?.also { task -> task.finishAndRemoveTask() } 905 } 906 907 override fun reloadVisibleExperience(manifestUrl: String, forceCache: Boolean): Boolean { 908 var activity: ExperienceActivity? = null 909 for (experienceActivityTask: ExperienceActivityTask in manifestUrlToExperienceActivityTask.values) { 910 if (manifestUrl == experienceActivityTask.manifestUrl) { 911 val weakActivity = 912 if (experienceActivityTask.experienceActivity == null) { 913 null 914 } else { 915 experienceActivityTask.experienceActivity!!.get() 916 } 917 activity = weakActivity 918 if (weakActivity == null) { 919 // No activity, just force a reload 920 break 921 } 922 Exponent.instance.runOnUiThread { weakActivity.startLoading() } 923 break 924 } 925 } 926 activity?.let { killActivityStack(it) } 927 openManifestUrl(manifestUrl, null, true, forceCache) 928 return true 929 } 930 931 override fun handleError(errorMessage: String) { 932 handleReactNativeError(developerErrorMessage(errorMessage), null, -1, true) 933 } 934 935 override fun handleError(exception: Exception) { 936 handleReactNativeError(ExceptionUtils.exceptionToErrorMessage(exception), null, -1, true, ExceptionUtils.exceptionToErrorHeader(exception)) 937 } 938 939 // TODO: probably need to call this from other places. 940 fun setHasError() { 941 hasError = true 942 } 943 944 companion object { 945 private val TAG = Kernel::class.java.simpleName 946 private lateinit var instance: Kernel 947 948 // Activities/Tasks 949 private val manifestUrlToExperienceActivityTask = mutableMapOf<String, ExperienceActivityTask>() 950 private val manifestUrlToOptions = mutableMapOf<String?, ExperienceOptions>() 951 private val manifestUrlToAppLoader = mutableMapOf<String?, ExpoUpdatesAppLoader>() 952 953 private fun addIntentDocumentFlags(intent: Intent) = intent.apply { 954 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 955 addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 956 addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) 957 } 958 959 @JvmStatic 960 @DoNotStrip 961 fun reloadVisibleExperience(activityId: Int) { 962 val manifestUrl = getManifestUrlForActivityId(activityId) 963 if (manifestUrl != null) { 964 instance.reloadVisibleExperience(manifestUrl, false) 965 } 966 } 967 968 // Called from DevServerHelper via ReactNativeStaticHelpers 969 @JvmStatic 970 @DoNotStrip 971 fun getManifestUrlForActivityId(activityId: Int): String? { 972 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.manifestUrl 973 } 974 975 // Called from DevServerHelper via ReactNativeStaticHelpers 976 @JvmStatic 977 @DoNotStrip 978 fun getBundleUrlForActivityId( 979 activityId: Int, 980 host: String, 981 mainModuleId: String?, 982 bundleTypeId: String?, 983 devMode: Boolean, 984 jsMinify: Boolean 985 ): String? { 986 // NOTE: This current implementation doesn't look at the bundleTypeId (see RN's private 987 // BundleType enum for the possible values) but may need to 988 if (activityId == -1) { 989 // This is the kernel 990 return instance.bundleUrl 991 } 992 if (InternalHeadlessAppLoader.hasBundleUrlForActivityId(activityId)) { 993 return InternalHeadlessAppLoader.getBundleUrlForActivityId(activityId) 994 } 995 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.bundleUrl 996 } 997 998 // <= SDK 25 999 @DoNotStrip 1000 fun getBundleUrlForActivityId( 1001 activityId: Int, 1002 host: String, 1003 jsModulePath: String?, 1004 devMode: Boolean, 1005 jsMinify: Boolean 1006 ): String? { 1007 if (activityId == -1) { 1008 // This is the kernel 1009 return instance.bundleUrl 1010 } 1011 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.bundleUrl 1012 } 1013 1014 // <= SDK 21 1015 @DoNotStrip 1016 fun getBundleUrlForActivityId( 1017 activityId: Int, 1018 host: String, 1019 jsModulePath: String?, 1020 devMode: Boolean, 1021 hmr: Boolean, 1022 jsMinify: Boolean 1023 ): String? { 1024 if (activityId == -1) { 1025 // This is the kernel 1026 return instance.bundleUrl 1027 } 1028 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.let { task -> 1029 var url = task.bundleUrl ?: return null 1030 if (hmr) { 1031 url = if (url.contains("hot=false")) { 1032 url.replace("hot=false", "hot=true") 1033 } else { 1034 "$url&hot=true" 1035 } 1036 } 1037 return url 1038 } 1039 } 1040 1041 /* 1042 * 1043 * Error handling 1044 * 1045 */ 1046 // Called using reflection from ReactAndroid. 1047 @DoNotStrip 1048 fun handleReactNativeError( 1049 errorMessage: String?, 1050 detailsUnversioned: Any?, 1051 exceptionId: Int?, 1052 isFatal: Boolean 1053 ) { 1054 handleReactNativeError( 1055 developerErrorMessage(errorMessage), 1056 detailsUnversioned, 1057 exceptionId, 1058 isFatal 1059 ) 1060 } 1061 1062 // Called using reflection from ReactAndroid. 1063 @DoNotStrip 1064 fun handleReactNativeError( 1065 throwable: Throwable?, 1066 errorMessage: String?, 1067 detailsUnversioned: Any?, 1068 exceptionId: Int?, 1069 isFatal: Boolean 1070 ) { 1071 handleReactNativeError( 1072 developerErrorMessage(errorMessage), 1073 detailsUnversioned, 1074 exceptionId, 1075 isFatal 1076 ) 1077 } 1078 1079 private fun handleReactNativeError( 1080 errorMessage: ExponentErrorMessage, 1081 detailsUnversioned: Any?, 1082 exceptionId: Int?, 1083 isFatal: Boolean, 1084 errorHeader: String? = null, 1085 ) { 1086 val stackList = ArrayList<Bundle>() 1087 if (detailsUnversioned != null) { 1088 val details = RNObject.wrap(detailsUnversioned) 1089 val arguments = RNObject("com.facebook.react.bridge.Arguments") 1090 arguments.loadVersion(details.version()) 1091 for (i in 0 until details.call("size") as Int) { 1092 try { 1093 val bundle = arguments.callStatic("toBundle", details.call("getMap", i)) as Bundle 1094 stackList.add(bundle) 1095 } catch (e: Exception) { 1096 e.printStackTrace() 1097 } 1098 } 1099 } else if (BuildConfig.DEBUG) { 1100 val stackTraceElements = Thread.currentThread().stackTrace 1101 // stackTraceElements starts with a bunch of stuff we don't care about. 1102 for (i in 2 until stackTraceElements.size) { 1103 val element = stackTraceElements[i] 1104 if (( 1105 (element.fileName != null) && element.fileName.startsWith(Kernel::class.java.simpleName) && 1106 ((element.methodName == "handleReactNativeError") || (element.methodName == "handleError")) 1107 ) 1108 ) { 1109 // Ignore these base error handling methods. 1110 continue 1111 } 1112 val bundle = Bundle().apply { 1113 putInt("column", 0) 1114 putInt("lineNumber", element.lineNumber) 1115 putString("methodName", element.methodName) 1116 putString("file", element.fileName) 1117 } 1118 stackList.add(bundle) 1119 } 1120 } 1121 val stack = stackList.toTypedArray() 1122 BaseExperienceActivity.addError( 1123 ExponentError( 1124 errorMessage, errorHeader, stack, 1125 getExceptionId(exceptionId), isFatal 1126 ) 1127 ) 1128 } 1129 1130 private fun getExceptionId(originalId: Int?): Int { 1131 return if (originalId == null || originalId == -1) { 1132 (-(Math.random() * Int.MAX_VALUE)).toInt() 1133 } else originalId 1134 } 1135 } 1136 1137 init { 1138 NativeModuleDepsProvider.instance.inject(Kernel::class.java, this) 1139 instance = this 1140 updateKernelRNOkHttp() 1141 } 1142 } 1143