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.kernelManifest 178 } catch (e: Throwable) { 179 Exponent.getInstance() 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.getInstance().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.getInstance().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.getInstance().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.kernelManifest, 269 HomeActivity.homeExpoPackages(), 270 initialURL 271 ) 272 ) 273 .addPackage( 274 ExpoTurboPackage.kernelExpoTurboPackage( 275 exponentManifest.kernelManifest, 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.kernelManifest.isDevelopmentMode()) { 286 Exponent.enableDeveloperSupport( 287 "UNVERSIONED", kernelDebuggerHost, 288 kernelMainModuleName, 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.kernelManifest.getDebuggerHost() 320 private val kernelMainModuleName: String 321 get() = exponentManifest.kernelManifest.getMainModuleName() 322 private val bundleUrl: String? 323 get() { 324 return try { 325 exponentManifest.kernelManifest.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.kernelManifest.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 = 532 ScopedNotificationsUtils.getExperienceScopeKey(response) ?: return false 533 val experience = ExponentDB.experienceScopeKeyToExperienceSync(experienceScopeKey) 534 if (experience == null) { 535 Log.w("expo-notifications", "Couldn't find experience from scopeKey: $experienceScopeKey") 536 return false 537 } 538 val manifestUrl = experience.manifestUrl 539 openExperience(ExperienceOptions(manifestUrl, manifestUrl, null)) 540 return true 541 } 542 543 private fun openDefaultUrl() { 544 val defaultUrl = 545 if (Constants.INITIAL_URL == null) KernelConstants.HOME_MANIFEST_URL else Constants.INITIAL_URL 546 openExperience(ExperienceOptions(defaultUrl, defaultUrl, null)) 547 } 548 549 override fun openExperience(options: ExperienceOptions) { 550 openManifestUrl(getManifestUrlFromFullUri(options.manifestUri), options, true) 551 } 552 553 private fun getManifestUrlFromFullUri(uriString: String?): String? { 554 if (uriString == null) { 555 return null 556 } 557 558 val uri = Uri.parse(uriString) 559 val builder = uri.buildUpon() 560 val deepLinkPositionDashes = 561 uriString.indexOf(ExponentManifest.DEEP_LINK_SEPARATOR_WITH_SLASH) 562 if (deepLinkPositionDashes >= 0) { 563 // do this safely so we preserve any query string 564 val pathSegments = uri.pathSegments 565 builder.path(null) 566 for (segment: String in pathSegments) { 567 if ((ExponentManifest.DEEP_LINK_SEPARATOR == segment)) { 568 break 569 } 570 builder.appendEncodedPath(segment) 571 } 572 } 573 574 // transfer the release-channel param to the built URL as this will cause Expo Go to treat 575 // this as a different project 576 var releaseChannel = uri.getQueryParameter(ExponentManifest.QUERY_PARAM_KEY_RELEASE_CHANNEL) 577 builder.query(null) 578 if (releaseChannel != null) { 579 // release channels cannot contain the ' ' character, so if this is present, 580 // it must be an encoded form of '+' which indicated a deep link in SDK <27. 581 // therefore, nothing after this is part of the release channel name so we should strip it. 582 // TODO: remove this check once SDK 26 and below are no longer supported 583 val releaseChannelDeepLinkPosition = releaseChannel.indexOf(' ') 584 if (releaseChannelDeepLinkPosition > -1) { 585 releaseChannel = releaseChannel.substring(0, releaseChannelDeepLinkPosition) 586 } 587 builder.appendQueryParameter( 588 ExponentManifest.QUERY_PARAM_KEY_RELEASE_CHANNEL, 589 releaseChannel 590 ) 591 } 592 593 // transfer the expo-updates query params: runtime-version, channel-name 594 val expoUpdatesQueryParameters = listOf( 595 ExponentManifest.QUERY_PARAM_KEY_EXPO_UPDATES_RUNTIME_VERSION, 596 ExponentManifest.QUERY_PARAM_KEY_EXPO_UPDATES_CHANNEL_NAME 597 ) 598 for (queryParameter: String in expoUpdatesQueryParameters) { 599 val queryParameterValue = uri.getQueryParameter(queryParameter) 600 if (queryParameterValue != null) { 601 builder.appendQueryParameter(queryParameter, queryParameterValue) 602 } 603 } 604 605 // ignore fragments as well (e.g. those added by auth-session) 606 builder.fragment(null) 607 var newUriString = builder.build().toString() 608 val deepLinkPositionPlus = newUriString.indexOf('+') 609 if (deepLinkPositionPlus >= 0 && deepLinkPositionDashes < 0) { 610 // need to keep this for backwards compatibility 611 newUriString = newUriString.substring(0, deepLinkPositionPlus) 612 } 613 614 // manifest url doesn't have a trailing slash 615 if (newUriString.isNotEmpty()) { 616 val lastUrlChar = newUriString[newUriString.length - 1] 617 if (lastUrlChar == '/') { 618 newUriString = newUriString.substring(0, newUriString.length - 1) 619 } 620 } 621 return newUriString 622 } 623 624 private fun openManifestUrl( 625 manifestUrl: String?, 626 options: ExperienceOptions?, 627 isOptimistic: Boolean, 628 forceCache: Boolean = false 629 ) { 630 SoLoader.init(context, false) 631 if (options == null) { 632 manifestUrlToOptions.remove(manifestUrl) 633 } else { 634 manifestUrlToOptions[manifestUrl] = options 635 } 636 if (manifestUrl == null || (manifestUrl == KernelConstants.HOME_MANIFEST_URL)) { 637 openHomeActivity() 638 return 639 } 640 if (Constants.isStandaloneApp()) { 641 openShellAppActivity(forceCache) 642 return 643 } 644 ErrorActivity.clearErrorList() 645 val tasks: List<AppTask> = experienceActivityTasks 646 var existingTask: AppTask? = null 647 for (i in tasks.indices) { 648 val task = tasks[i] 649 val baseIntent = task.taskInfo.baseIntent 650 if (baseIntent.hasExtra(KernelConstants.MANIFEST_URL_KEY) && ( 651 baseIntent.getStringExtra( 652 KernelConstants.MANIFEST_URL_KEY 653 ) == manifestUrl 654 ) 655 ) { 656 existingTask = task 657 break 658 } 659 } 660 if (isOptimistic && existingTask == null) { 661 openOptimisticExperienceActivity(manifestUrl) 662 } 663 if (existingTask != null) { 664 try { 665 moveTaskToFront(existingTask.taskInfo.id) 666 } catch (e: IllegalArgumentException) { 667 // Sometimes task can't be found. 668 existingTask = null 669 openOptimisticExperienceActivity(manifestUrl) 670 } 671 } 672 val finalExistingTask = existingTask 673 if (existingTask == null) { 674 ExpoUpdatesAppLoader( 675 manifestUrl, 676 object : AppLoaderCallback { 677 override fun onOptimisticManifest(optimisticManifest: RawManifest) { 678 Exponent.getInstance() 679 .runOnUiThread { sendOptimisticManifestToExperienceActivity(optimisticManifest) } 680 } 681 682 override fun onManifestCompleted(manifest: RawManifest) { 683 Exponent.getInstance().runOnUiThread { 684 try { 685 openManifestUrlStep2(manifestUrl, manifest, finalExistingTask) 686 } catch (e: JSONException) { 687 handleError(e) 688 } 689 } 690 } 691 692 override fun onBundleCompleted(localBundlePath: String) { 693 Exponent.getInstance().runOnUiThread { sendBundleToExperienceActivity(localBundlePath) } 694 } 695 696 override fun emitEvent(params: JSONObject) { 697 val task = manifestUrlToExperienceActivityTask[manifestUrl] 698 if (task != null) { 699 val experienceActivity = task.experienceActivity!!.get() 700 experienceActivity?.emitUpdatesEvent(params) 701 } 702 } 703 704 override fun updateStatus(status: AppLoaderStatus) { 705 if (optimisticActivity != null) { 706 optimisticActivity!!.setLoadingProgressStatusIfEnabled(status) 707 } 708 } 709 710 override fun onError(e: Exception) { 711 Exponent.getInstance().runOnUiThread { handleError(e) } 712 } 713 }, 714 forceCache 715 ).start(context) 716 } 717 } 718 719 @Throws(JSONException::class) 720 private fun openManifestUrlStep2( 721 manifestUrl: String, 722 manifest: RawManifest, 723 existingTask: AppTask? 724 ) { 725 val bundleUrl = toHttp(manifest.getBundleURL()) 726 val task = getExperienceActivityTask(manifestUrl) 727 task.bundleUrl = bundleUrl 728 ExponentManifest.normalizeRawManifestInPlace(manifest, manifestUrl) 729 val opts = JSONObject() 730 if (existingTask == null) { 731 sendManifestToExperienceActivity(manifestUrl, manifest, bundleUrl, opts) 732 } 733 val params = Arguments.createMap().apply { 734 putString("manifestUrl", manifestUrl) 735 putString("manifestString", manifest.toString()) 736 } 737 queueEvent( 738 "ExponentKernel.addHistoryItem", params, 739 object : KernelEventCallback { 740 override fun onEventSuccess(result: ReadableMap) { 741 EXL.d(TAG, "Successfully called ExponentKernel.addHistoryItem in kernel JS.") 742 } 743 744 override fun onEventFailure(errorMessage: String?) { 745 EXL.e(TAG, "Error calling ExponentKernel.addHistoryItem in kernel JS: $errorMessage") 746 } 747 } 748 ) 749 killOrphanedLauncherActivities() 750 } 751 752 /* 753 * 754 * Optimistic experiences 755 * 756 */ 757 private fun openOptimisticExperienceActivity(manifestUrl: String?) { 758 try { 759 val intent = Intent(activityContext, ExperienceActivity::class.java).apply { 760 addIntentDocumentFlags(this) 761 putExtra(KernelConstants.MANIFEST_URL_KEY, manifestUrl) 762 putExtra(KernelConstants.IS_OPTIMISTIC_KEY, true) 763 } 764 activityContext!!.startActivity(intent) 765 } catch (e: Throwable) { 766 EXL.e(TAG, e) 767 } 768 } 769 770 fun setOptimisticActivity(experienceActivity: ExperienceActivity, taskId: Int) { 771 optimisticActivity = experienceActivity 772 optimisticTaskId = taskId 773 AsyncCondition.notify(KernelConstants.OPEN_OPTIMISTIC_EXPERIENCE_ACTIVITY_KEY) 774 AsyncCondition.notify(KernelConstants.OPEN_EXPERIENCE_ACTIVITY_KEY) 775 } 776 777 fun sendOptimisticManifestToExperienceActivity(optimisticManifest: RawManifest?) { 778 AsyncCondition.wait( 779 KernelConstants.OPEN_OPTIMISTIC_EXPERIENCE_ACTIVITY_KEY, 780 object : AsyncConditionListener { 781 override fun isReady(): Boolean { 782 return optimisticActivity != null && optimisticTaskId != null 783 } 784 785 override fun execute() { 786 optimisticActivity!!.setOptimisticManifest(optimisticManifest) 787 } 788 } 789 ) 790 } 791 792 private fun sendManifestToExperienceActivity( 793 manifestUrl: String?, 794 manifest: RawManifest?, 795 bundleUrl: String?, 796 kernelOptions: JSONObject? 797 ) { 798 AsyncCondition.wait( 799 KernelConstants.OPEN_EXPERIENCE_ACTIVITY_KEY, 800 object : AsyncConditionListener { 801 override fun isReady(): Boolean { 802 return optimisticActivity != null && optimisticTaskId != null 803 } 804 805 override fun execute() { 806 optimisticActivity!!.setManifest(manifestUrl, manifest, bundleUrl, kernelOptions) 807 AsyncCondition.notify(KernelConstants.LOAD_BUNDLE_FOR_EXPERIENCE_ACTIVITY_KEY) 808 } 809 } 810 ) 811 } 812 813 fun sendBundleToExperienceActivity(localBundlePath: String?) { 814 AsyncCondition.wait( 815 KernelConstants.LOAD_BUNDLE_FOR_EXPERIENCE_ACTIVITY_KEY, 816 object : AsyncConditionListener { 817 override fun isReady(): Boolean { 818 return optimisticActivity != null && optimisticTaskId != null 819 } 820 821 override fun execute() { 822 optimisticActivity!!.setBundle(localBundlePath) 823 optimisticActivity = null 824 optimisticTaskId = null 825 } 826 } 827 ) 828 } 829 830 /* 831 * 832 * Tasks 833 * 834 */ 835 val tasks: List<AppTask> 836 get() { 837 val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 838 return manager.appTasks 839 } 840 841 // Get list of tasks in our format. 842 val experienceActivityTasks: List<AppTask> 843 get() = tasks 844 845 // Sometimes LauncherActivity.finish() doesn't close the activity and task. Not sure why exactly. 846 // Thought it was related to launchMode="singleTask" but other launchModes seem to have the same problem. 847 // This can be reproduced by creating a shortcut, exiting app, clicking on shortcut, refreshing, pressing 848 // home, clicking on shortcut, click recent apps button. There will be a blank LauncherActivity behind 849 // the ExperienceActivity. killOrphanedLauncherActivities solves this but would be nice to figure out 850 // the root cause. 851 private fun killOrphanedLauncherActivities() { 852 try { 853 // Crash with NoSuchFieldException instead of hard crashing at taskInfo.numActivities 854 RecentTaskInfo::class.java.getDeclaredField("numActivities") 855 for (task: AppTask in tasks) { 856 val taskInfo = task.taskInfo 857 if (taskInfo.numActivities == 0 && (taskInfo.baseIntent.action == Intent.ACTION_MAIN)) { 858 task.finishAndRemoveTask() 859 return 860 } 861 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 862 if (taskInfo.numActivities == 1 && (taskInfo.topActivity!!.className == LauncherActivity::class.java.name)) { 863 task.finishAndRemoveTask() 864 return 865 } 866 } 867 } 868 } catch (e: NoSuchFieldException) { 869 // Don't EXL here because this isn't actually a problem 870 Log.e(TAG, e.toString()) 871 } catch (e: Throwable) { 872 EXL.e(TAG, e) 873 } 874 } 875 876 fun moveTaskToFront(taskId: Int) { 877 tasks.find { it.taskInfo.id == taskId }?.also { task -> 878 // If we have the task in memory, tell the ExperienceActivity to check for new options. 879 // Otherwise options will be added in initialProps when the Experience starts. 880 val exponentTask = experienceActivityTaskForTaskId(taskId) 881 if (exponentTask != null) { 882 val experienceActivity = exponentTask.experienceActivity!!.get() 883 experienceActivity?.shouldCheckOptions() 884 } 885 task.moveToFront() 886 } 887 } 888 889 fun killActivityStack(activity: Activity) { 890 val exponentTask = experienceActivityTaskForTaskId(activity.taskId) 891 if (exponentTask != null) { 892 removeExperienceActivityTask(exponentTask.manifestUrl) 893 } 894 895 // Kill the current task. 896 val manager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 897 manager.appTasks.find { it.taskInfo.id == activity.taskId }?.also { task -> task.finishAndRemoveTask() } 898 } 899 900 override fun reloadVisibleExperience(manifestUrl: String, forceCache: Boolean): Boolean { 901 var activity: ExperienceActivity? = null 902 for (experienceActivityTask: ExperienceActivityTask in manifestUrlToExperienceActivityTask.values) { 903 if (manifestUrl == experienceActivityTask.manifestUrl) { 904 val weakActivity = 905 if (experienceActivityTask.experienceActivity == null) { 906 null 907 } else { 908 experienceActivityTask.experienceActivity!!.get() 909 } 910 activity = weakActivity 911 if (weakActivity == null) { 912 // No activity, just force a reload 913 break 914 } 915 Exponent.getInstance().runOnUiThread { weakActivity.startLoading() } 916 break 917 } 918 } 919 activity?.let { killActivityStack(it) } 920 openManifestUrl(manifestUrl, null, true, forceCache) 921 return true 922 } 923 924 override fun handleError(errorMessage: String) { 925 handleReactNativeError(developerErrorMessage(errorMessage), null, -1, true) 926 } 927 928 override fun handleError(exception: Exception) { 929 handleReactNativeError(ExceptionUtils.exceptionToErrorMessage(exception), null, -1, true) 930 } 931 932 // TODO: probably need to call this from other places. 933 fun setHasError() { 934 hasError = true 935 } 936 937 companion object { 938 private val TAG = Kernel::class.java.simpleName 939 private lateinit var instance: Kernel 940 941 // Activities/Tasks 942 private val manifestUrlToExperienceActivityTask = mutableMapOf<String, ExperienceActivityTask>() 943 private val manifestUrlToOptions = mutableMapOf<String?, ExperienceOptions>() 944 private val manifestUrlToAppLoader = mutableMapOf<String?, ExpoUpdatesAppLoader>() 945 946 private fun addIntentDocumentFlags(intent: Intent) = intent.apply { 947 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 948 addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 949 addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) 950 } 951 952 @JvmStatic 953 @DoNotStrip 954 fun reloadVisibleExperience(activityId: Int) { 955 val manifestUrl = getManifestUrlForActivityId(activityId) 956 if (manifestUrl != null) { 957 instance.reloadVisibleExperience(manifestUrl, false) 958 } 959 } 960 961 // Called from DevServerHelper via ReactNativeStaticHelpers 962 @JvmStatic 963 @DoNotStrip 964 fun getManifestUrlForActivityId(activityId: Int): String? { 965 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.manifestUrl 966 } 967 968 // Called from DevServerHelper via ReactNativeStaticHelpers 969 @JvmStatic 970 @DoNotStrip 971 fun getBundleUrlForActivityId( 972 activityId: Int, 973 host: String, 974 mainModuleId: String?, 975 bundleTypeId: String?, 976 devMode: Boolean, 977 jsMinify: Boolean 978 ): String? { 979 // NOTE: This current implementation doesn't look at the bundleTypeId (see RN's private 980 // BundleType enum for the possible values) but may need to 981 if (activityId == -1) { 982 // This is the kernel 983 return instance.bundleUrl 984 } 985 if (InternalHeadlessAppLoader.hasBundleUrlForActivityId(activityId)) { 986 return InternalHeadlessAppLoader.getBundleUrlForActivityId(activityId) 987 } 988 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.bundleUrl 989 } 990 991 // <= SDK 25 992 @DoNotStrip 993 fun getBundleUrlForActivityId( 994 activityId: Int, 995 host: String, 996 jsModulePath: String?, 997 devMode: Boolean, 998 jsMinify: Boolean 999 ): String? { 1000 if (activityId == -1) { 1001 // This is the kernel 1002 return instance.bundleUrl 1003 } 1004 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.bundleUrl 1005 } 1006 1007 // <= SDK 21 1008 @DoNotStrip 1009 fun getBundleUrlForActivityId( 1010 activityId: Int, 1011 host: String, 1012 jsModulePath: String?, 1013 devMode: Boolean, 1014 hmr: Boolean, 1015 jsMinify: Boolean 1016 ): String? { 1017 if (activityId == -1) { 1018 // This is the kernel 1019 return instance.bundleUrl 1020 } 1021 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.let { task -> 1022 var url = task.bundleUrl ?: return null 1023 if (hmr) { 1024 url = if (url.contains("hot=false")) { 1025 url.replace("hot=false", "hot=true") 1026 } else { 1027 "$url&hot=true" 1028 } 1029 } 1030 return url 1031 } 1032 } 1033 1034 /* 1035 * 1036 * Error handling 1037 * 1038 */ 1039 // Called using reflection from ReactAndroid. 1040 @DoNotStrip 1041 fun handleReactNativeError( 1042 errorMessage: String?, 1043 detailsUnversioned: Any?, 1044 exceptionId: Int?, 1045 isFatal: Boolean 1046 ) { 1047 handleReactNativeError( 1048 developerErrorMessage(errorMessage), 1049 detailsUnversioned, 1050 exceptionId, 1051 isFatal 1052 ) 1053 } 1054 1055 // Called using reflection from ReactAndroid. 1056 @DoNotStrip 1057 fun handleReactNativeError( 1058 throwable: Throwable?, 1059 errorMessage: String?, 1060 detailsUnversioned: Any?, 1061 exceptionId: Int?, 1062 isFatal: Boolean 1063 ) { 1064 handleReactNativeError( 1065 developerErrorMessage(errorMessage), 1066 detailsUnversioned, 1067 exceptionId, 1068 isFatal 1069 ) 1070 } 1071 1072 private fun handleReactNativeError( 1073 errorMessage: ExponentErrorMessage, 1074 detailsUnversioned: Any?, 1075 exceptionId: Int?, 1076 isFatal: Boolean 1077 ) { 1078 val stackList = ArrayList<Bundle>() 1079 if (detailsUnversioned != null) { 1080 val details = RNObject.wrap(detailsUnversioned) 1081 val arguments = RNObject("com.facebook.react.bridge.Arguments") 1082 arguments.loadVersion(details.version()) 1083 for (i in 0 until details.call("size") as Int) { 1084 try { 1085 val bundle = arguments.callStatic("toBundle", details.call("getMap", i)) as Bundle 1086 stackList.add(bundle) 1087 } catch (e: Exception) { 1088 e.printStackTrace() 1089 } 1090 } 1091 } else if (BuildConfig.DEBUG) { 1092 val stackTraceElements = Thread.currentThread().stackTrace 1093 // stackTraceElements starts with a bunch of stuff we don't care about. 1094 for (i in 2 until stackTraceElements.size) { 1095 val element = stackTraceElements[i] 1096 if (( 1097 (element.fileName != null) && element.fileName.startsWith(Kernel::class.java.simpleName) && 1098 ((element.methodName == "handleReactNativeError") || (element.methodName == "handleError")) 1099 ) 1100 ) { 1101 // Ignore these base error handling methods. 1102 continue 1103 } 1104 val bundle = Bundle().apply { 1105 putInt("column", 0) 1106 putInt("lineNumber", element.lineNumber) 1107 putString("methodName", element.methodName) 1108 putString("file", element.fileName) 1109 } 1110 stackList.add(bundle) 1111 } 1112 } 1113 val stack = stackList.toTypedArray() 1114 BaseExperienceActivity.addError( 1115 ExponentError( 1116 errorMessage, stack, 1117 getExceptionId(exceptionId), isFatal 1118 ) 1119 ) 1120 } 1121 1122 private fun getExceptionId(originalId: Int?): Int { 1123 return if (originalId == null || originalId == -1) { 1124 (-(Math.random() * Int.MAX_VALUE)).toInt() 1125 } else originalId 1126 } 1127 } 1128 1129 init { 1130 NativeModuleDepsProvider.getInstance().inject(Kernel::class.java, this) 1131 instance = this 1132 updateKernelRNOkHttp() 1133 } 1134 } 1135