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