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.internal.BundleJSONConverter 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.JavaScriptContextHolder 25 import com.facebook.react.bridge.ReactApplicationContext 26 import com.facebook.react.bridge.ReadableMap 27 import com.facebook.react.common.LifecycleState 28 import com.facebook.react.modules.network.ReactCookieJarContainer 29 import com.facebook.react.shell.MainReactPackage 30 import com.facebook.soloader.SoLoader 31 import de.greenrobot.event.EventBus 32 import expo.modules.notifications.service.NotificationsService.Companion.getNotificationResponseFromIntent 33 import expo.modules.notifications.service.delegates.ExpoHandlingDelegate 34 import expo.modules.updates.manifest.raw.RawManifest 35 import host.exp.exponent.* 36 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderCallback 37 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderStatus 38 import host.exp.exponent.analytics.EXL 39 import host.exp.exponent.di.NativeModuleDepsProvider 40 import host.exp.exponent.exceptions.ExceptionUtils 41 import host.exp.exponent.experience.BaseExperienceActivity 42 import host.exp.exponent.experience.ErrorActivity 43 import host.exp.exponent.experience.ExperienceActivity 44 import host.exp.exponent.experience.HomeActivity 45 import host.exp.exponent.headless.InternalHeadlessAppLoader 46 import host.exp.exponent.kernel.ExponentErrorMessage.Companion.developerErrorMessage 47 import host.exp.exponent.kernel.ExponentKernelModuleProvider.KernelEventCallback 48 import host.exp.exponent.kernel.ExponentKernelModuleProvider.queueEvent 49 import host.exp.exponent.kernel.ExponentUrls.toHttp 50 import host.exp.exponent.kernel.KernelConstants.ExperienceOptions 51 import host.exp.exponent.network.ExponentNetwork 52 import host.exp.exponent.notifications.ExponentNotification 53 import host.exp.exponent.notifications.ExponentNotificationManager 54 import host.exp.exponent.notifications.NotificationActionCenter 55 import host.exp.exponent.notifications.ScopedNotificationsUtils 56 import host.exp.exponent.storage.ExponentDB 57 import host.exp.exponent.storage.ExponentSharedPreferences 58 import host.exp.exponent.utils.AsyncCondition 59 import host.exp.exponent.utils.AsyncCondition.AsyncConditionListener 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.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.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.SHOULD_NOT_USE_KERNEL_CACHE) 231 if (!ExpoViewBuildConfig.DEBUG) { 232 val oldKernelRevisionId = 233 exponentSharedPreferences.getString(ExponentSharedPreferences.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.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.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.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.setActionType(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.setInputText(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 = getNotificationResponseFromIntent(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? = null 652 for (i in tasks.indices) { 653 val task = tasks[i] 654 val baseIntent = task.taskInfo.baseIntent 655 if (baseIntent.hasExtra(KernelConstants.MANIFEST_URL_KEY) && ( 656 baseIntent.getStringExtra( 657 KernelConstants.MANIFEST_URL_KEY 658 ) == manifestUrl 659 ) 660 ) { 661 existingTask = task 662 break 663 } 664 } 665 if (isOptimistic && existingTask == null) { 666 openOptimisticExperienceActivity(manifestUrl) 667 } 668 if (existingTask != null) { 669 try { 670 moveTaskToFront(existingTask.taskInfo.id) 671 } catch (e: IllegalArgumentException) { 672 // Sometimes task can't be found. 673 existingTask = null 674 openOptimisticExperienceActivity(manifestUrl) 675 } 676 } 677 val finalExistingTask = existingTask 678 if (existingTask == null) { 679 ExpoUpdatesAppLoader( 680 manifestUrl, 681 object : AppLoaderCallback { 682 override fun onOptimisticManifest(optimisticManifest: RawManifest) { 683 Exponent.instance 684 .runOnUiThread { sendOptimisticManifestToExperienceActivity(optimisticManifest) } 685 } 686 687 override fun onManifestCompleted(manifest: RawManifest) { 688 Exponent.instance.runOnUiThread { 689 try { 690 openManifestUrlStep2(manifestUrl, manifest, finalExistingTask) 691 } catch (e: JSONException) { 692 handleError(e) 693 } 694 } 695 } 696 697 override fun onBundleCompleted(localBundlePath: String?) { 698 Exponent.instance.runOnUiThread { sendBundleToExperienceActivity(localBundlePath) } 699 } 700 701 override fun emitEvent(params: JSONObject) { 702 val task = manifestUrlToExperienceActivityTask[manifestUrl] 703 if (task != null) { 704 val experienceActivity = task.experienceActivity!!.get() 705 experienceActivity?.emitUpdatesEvent(params) 706 } 707 } 708 709 override fun updateStatus(status: AppLoaderStatus) { 710 if (optimisticActivity != null) { 711 optimisticActivity!!.setLoadingProgressStatusIfEnabled(status) 712 } 713 } 714 715 override fun onError(e: Exception) { 716 Exponent.instance.runOnUiThread { handleError(e) } 717 } 718 }, 719 forceCache 720 ).start(context) 721 } 722 } 723 724 @Throws(JSONException::class) 725 private fun openManifestUrlStep2( 726 manifestUrl: String, 727 manifest: RawManifest, 728 existingTask: AppTask? 729 ) { 730 val bundleUrl = toHttp(manifest.getBundleURL()) 731 val task = getExperienceActivityTask(manifestUrl) 732 task.bundleUrl = bundleUrl 733 ExponentManifest.normalizeRawManifestInPlace(manifest, manifestUrl) 734 if (existingTask == null) { 735 sendManifestToExperienceActivity(manifestUrl, manifest, bundleUrl) 736 } 737 val params = Arguments.createMap().apply { 738 putString("manifestUrl", manifestUrl) 739 putString("manifestString", manifest.toString()) 740 } 741 queueEvent( 742 "ExponentKernel.addHistoryItem", params, 743 object : KernelEventCallback { 744 override fun onEventSuccess(result: ReadableMap) { 745 EXL.d(TAG, "Successfully called ExponentKernel.addHistoryItem in kernel JS.") 746 } 747 748 override fun onEventFailure(errorMessage: String?) { 749 EXL.e(TAG, "Error calling ExponentKernel.addHistoryItem in kernel JS: $errorMessage") 750 } 751 } 752 ) 753 killOrphanedLauncherActivities() 754 } 755 756 /* 757 * 758 * Optimistic experiences 759 * 760 */ 761 private fun openOptimisticExperienceActivity(manifestUrl: String?) { 762 try { 763 val intent = Intent(activityContext, ExperienceActivity::class.java).apply { 764 addIntentDocumentFlags(this) 765 putExtra(KernelConstants.MANIFEST_URL_KEY, manifestUrl) 766 putExtra(KernelConstants.IS_OPTIMISTIC_KEY, true) 767 } 768 activityContext!!.startActivity(intent) 769 } catch (e: Throwable) { 770 EXL.e(TAG, e) 771 } 772 } 773 774 fun setOptimisticActivity(experienceActivity: ExperienceActivity, taskId: Int) { 775 optimisticActivity = experienceActivity 776 optimisticTaskId = taskId 777 AsyncCondition.notify(KernelConstants.OPEN_OPTIMISTIC_EXPERIENCE_ACTIVITY_KEY) 778 AsyncCondition.notify(KernelConstants.OPEN_EXPERIENCE_ACTIVITY_KEY) 779 } 780 781 fun sendOptimisticManifestToExperienceActivity(optimisticManifest: RawManifest) { 782 AsyncCondition.wait( 783 KernelConstants.OPEN_OPTIMISTIC_EXPERIENCE_ACTIVITY_KEY, 784 object : AsyncConditionListener { 785 override fun isReady(): Boolean { 786 return optimisticActivity != null && optimisticTaskId != null 787 } 788 789 override fun execute() { 790 optimisticActivity!!.setOptimisticManifest(optimisticManifest) 791 } 792 } 793 ) 794 } 795 796 private fun sendManifestToExperienceActivity( 797 manifestUrl: String, 798 manifest: RawManifest, 799 bundleUrl: String, 800 ) { 801 AsyncCondition.wait( 802 KernelConstants.OPEN_EXPERIENCE_ACTIVITY_KEY, 803 object : AsyncConditionListener { 804 override fun isReady(): Boolean { 805 return optimisticActivity != null && optimisticTaskId != null 806 } 807 808 override fun execute() { 809 optimisticActivity!!.setManifest(manifestUrl, manifest, bundleUrl) 810 AsyncCondition.notify(KernelConstants.LOAD_BUNDLE_FOR_EXPERIENCE_ACTIVITY_KEY) 811 } 812 } 813 ) 814 } 815 816 fun sendBundleToExperienceActivity(localBundlePath: String?) { 817 AsyncCondition.wait( 818 KernelConstants.LOAD_BUNDLE_FOR_EXPERIENCE_ACTIVITY_KEY, 819 object : AsyncConditionListener { 820 override fun isReady(): Boolean { 821 return optimisticActivity != null && optimisticTaskId != null 822 } 823 824 override fun execute() { 825 optimisticActivity!!.setBundle(localBundlePath) 826 optimisticActivity = null 827 optimisticTaskId = null 828 } 829 } 830 ) 831 } 832 833 /* 834 * 835 * Tasks 836 * 837 */ 838 val tasks: List<AppTask> 839 get() { 840 val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 841 return manager.appTasks 842 } 843 844 // Get list of tasks in our format. 845 val experienceActivityTasks: List<AppTask> 846 get() = tasks 847 848 // Sometimes LauncherActivity.finish() doesn't close the activity and task. Not sure why exactly. 849 // Thought it was related to launchMode="singleTask" but other launchModes seem to have the same problem. 850 // This can be reproduced by creating a shortcut, exiting app, clicking on shortcut, refreshing, pressing 851 // home, clicking on shortcut, click recent apps button. There will be a blank LauncherActivity behind 852 // the ExperienceActivity. killOrphanedLauncherActivities solves this but would be nice to figure out 853 // the root cause. 854 private fun killOrphanedLauncherActivities() { 855 try { 856 // Crash with NoSuchFieldException instead of hard crashing at taskInfo.numActivities 857 RecentTaskInfo::class.java.getDeclaredField("numActivities") 858 for (task: AppTask in tasks) { 859 val taskInfo = task.taskInfo 860 if (taskInfo.numActivities == 0 && (taskInfo.baseIntent.action == Intent.ACTION_MAIN)) { 861 task.finishAndRemoveTask() 862 return 863 } 864 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 865 if (taskInfo.numActivities == 1 && (taskInfo.topActivity!!.className == LauncherActivity::class.java.name)) { 866 task.finishAndRemoveTask() 867 return 868 } 869 } 870 } 871 } catch (e: NoSuchFieldException) { 872 // Don't EXL here because this isn't actually a problem 873 Log.e(TAG, e.toString()) 874 } catch (e: Throwable) { 875 EXL.e(TAG, e) 876 } 877 } 878 879 fun moveTaskToFront(taskId: Int) { 880 tasks.find { it.taskInfo.id == taskId }?.also { task -> 881 // If we have the task in memory, tell the ExperienceActivity to check for new options. 882 // Otherwise options will be added in initialProps when the Experience starts. 883 val exponentTask = experienceActivityTaskForTaskId(taskId) 884 if (exponentTask != null) { 885 val experienceActivity = exponentTask.experienceActivity!!.get() 886 experienceActivity?.shouldCheckOptions() 887 } 888 task.moveToFront() 889 } 890 } 891 892 fun killActivityStack(activity: Activity) { 893 val exponentTask = experienceActivityTaskForTaskId(activity.taskId) 894 if (exponentTask != null) { 895 removeExperienceActivityTask(exponentTask.manifestUrl) 896 } 897 898 // Kill the current task. 899 val manager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 900 manager.appTasks.find { it.taskInfo.id == activity.taskId }?.also { task -> task.finishAndRemoveTask() } 901 } 902 903 override fun reloadVisibleExperience(manifestUrl: String, forceCache: Boolean): Boolean { 904 var activity: ExperienceActivity? = null 905 for (experienceActivityTask: ExperienceActivityTask in manifestUrlToExperienceActivityTask.values) { 906 if (manifestUrl == experienceActivityTask.manifestUrl) { 907 val weakActivity = 908 if (experienceActivityTask.experienceActivity == null) { 909 null 910 } else { 911 experienceActivityTask.experienceActivity!!.get() 912 } 913 activity = weakActivity 914 if (weakActivity == null) { 915 // No activity, just force a reload 916 break 917 } 918 Exponent.instance.runOnUiThread { weakActivity.startLoading() } 919 break 920 } 921 } 922 activity?.let { killActivityStack(it) } 923 openManifestUrl(manifestUrl, null, true, forceCache) 924 return true 925 } 926 927 override fun handleError(errorMessage: String) { 928 handleReactNativeError(developerErrorMessage(errorMessage), null, -1, true) 929 } 930 931 override fun handleError(exception: Exception) { 932 handleReactNativeError(ExceptionUtils.exceptionToErrorMessage(exception), null, -1, true) 933 } 934 935 // TODO: probably need to call this from other places. 936 fun setHasError() { 937 hasError = true 938 } 939 940 companion object { 941 private val TAG = Kernel::class.java.simpleName 942 private lateinit var instance: Kernel 943 944 // Activities/Tasks 945 private val manifestUrlToExperienceActivityTask = mutableMapOf<String, ExperienceActivityTask>() 946 private val manifestUrlToOptions = mutableMapOf<String?, ExperienceOptions>() 947 private val manifestUrlToAppLoader = mutableMapOf<String?, ExpoUpdatesAppLoader>() 948 949 private fun addIntentDocumentFlags(intent: Intent) = intent.apply { 950 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 951 addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 952 addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) 953 } 954 955 @JvmStatic 956 @DoNotStrip 957 fun reloadVisibleExperience(activityId: Int) { 958 val manifestUrl = getManifestUrlForActivityId(activityId) 959 if (manifestUrl != null) { 960 instance.reloadVisibleExperience(manifestUrl, false) 961 } 962 } 963 964 // Called from DevServerHelper via ReactNativeStaticHelpers 965 @JvmStatic 966 @DoNotStrip 967 fun getManifestUrlForActivityId(activityId: Int): String? { 968 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.manifestUrl 969 } 970 971 // Called from DevServerHelper via ReactNativeStaticHelpers 972 @JvmStatic 973 @DoNotStrip 974 fun getBundleUrlForActivityId( 975 activityId: Int, 976 host: String, 977 mainModuleId: String?, 978 bundleTypeId: String?, 979 devMode: Boolean, 980 jsMinify: Boolean 981 ): String? { 982 // NOTE: This current implementation doesn't look at the bundleTypeId (see RN's private 983 // BundleType enum for the possible values) but may need to 984 if (activityId == -1) { 985 // This is the kernel 986 return instance.bundleUrl 987 } 988 if (InternalHeadlessAppLoader.hasBundleUrlForActivityId(activityId)) { 989 return InternalHeadlessAppLoader.getBundleUrlForActivityId(activityId) 990 } 991 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.bundleUrl 992 } 993 994 // <= SDK 25 995 @DoNotStrip 996 fun getBundleUrlForActivityId( 997 activityId: Int, 998 host: String, 999 jsModulePath: String?, 1000 devMode: Boolean, 1001 jsMinify: Boolean 1002 ): String? { 1003 if (activityId == -1) { 1004 // This is the kernel 1005 return instance.bundleUrl 1006 } 1007 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.bundleUrl 1008 } 1009 1010 // <= SDK 21 1011 @DoNotStrip 1012 fun getBundleUrlForActivityId( 1013 activityId: Int, 1014 host: String, 1015 jsModulePath: String?, 1016 devMode: Boolean, 1017 hmr: Boolean, 1018 jsMinify: Boolean 1019 ): String? { 1020 if (activityId == -1) { 1021 // This is the kernel 1022 return instance.bundleUrl 1023 } 1024 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.let { task -> 1025 var url = task.bundleUrl ?: return null 1026 if (hmr) { 1027 url = if (url.contains("hot=false")) { 1028 url.replace("hot=false", "hot=true") 1029 } else { 1030 "$url&hot=true" 1031 } 1032 } 1033 return url 1034 } 1035 } 1036 1037 /* 1038 * 1039 * Error handling 1040 * 1041 */ 1042 // Called using reflection from ReactAndroid. 1043 @DoNotStrip 1044 fun handleReactNativeError( 1045 errorMessage: String?, 1046 detailsUnversioned: Any?, 1047 exceptionId: Int?, 1048 isFatal: Boolean 1049 ) { 1050 handleReactNativeError( 1051 developerErrorMessage(errorMessage), 1052 detailsUnversioned, 1053 exceptionId, 1054 isFatal 1055 ) 1056 } 1057 1058 // Called using reflection from ReactAndroid. 1059 @DoNotStrip 1060 fun handleReactNativeError( 1061 throwable: Throwable?, 1062 errorMessage: String?, 1063 detailsUnversioned: Any?, 1064 exceptionId: Int?, 1065 isFatal: Boolean 1066 ) { 1067 handleReactNativeError( 1068 developerErrorMessage(errorMessage), 1069 detailsUnversioned, 1070 exceptionId, 1071 isFatal 1072 ) 1073 } 1074 1075 private fun handleReactNativeError( 1076 errorMessage: ExponentErrorMessage, 1077 detailsUnversioned: Any?, 1078 exceptionId: Int?, 1079 isFatal: Boolean 1080 ) { 1081 val stackList = ArrayList<Bundle>() 1082 if (detailsUnversioned != null) { 1083 val details = RNObject.wrap(detailsUnversioned) 1084 val arguments = RNObject("com.facebook.react.bridge.Arguments") 1085 arguments.loadVersion(details.version()) 1086 for (i in 0 until details.call("size") as Int) { 1087 try { 1088 val bundle = arguments.callStatic("toBundle", details.call("getMap", i)) as Bundle 1089 stackList.add(bundle) 1090 } catch (e: Exception) { 1091 e.printStackTrace() 1092 } 1093 } 1094 } else if (BuildConfig.DEBUG) { 1095 val stackTraceElements = Thread.currentThread().stackTrace 1096 // stackTraceElements starts with a bunch of stuff we don't care about. 1097 for (i in 2 until stackTraceElements.size) { 1098 val element = stackTraceElements[i] 1099 if (( 1100 (element.fileName != null) && element.fileName.startsWith(Kernel::class.java.simpleName) && 1101 ((element.methodName == "handleReactNativeError") || (element.methodName == "handleError")) 1102 ) 1103 ) { 1104 // Ignore these base error handling methods. 1105 continue 1106 } 1107 val bundle = Bundle().apply { 1108 putInt("column", 0) 1109 putInt("lineNumber", element.lineNumber) 1110 putString("methodName", element.methodName) 1111 putString("file", element.fileName) 1112 } 1113 stackList.add(bundle) 1114 } 1115 } 1116 val stack = stackList.toTypedArray() 1117 BaseExperienceActivity.addError( 1118 ExponentError( 1119 errorMessage, stack, 1120 getExceptionId(exceptionId), isFatal 1121 ) 1122 ) 1123 } 1124 1125 private fun getExceptionId(originalId: Int?): Int { 1126 return if (originalId == null || originalId == -1) { 1127 (-(Math.random() * Int.MAX_VALUE)).toInt() 1128 } else originalId 1129 } 1130 } 1131 1132 init { 1133 NativeModuleDepsProvider.getInstance().inject(Kernel::class.java, this) 1134 instance = this 1135 updateKernelRNOkHttp() 1136 } 1137 } 1138