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