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