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.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.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 "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.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.getInstance() 684 .runOnUiThread { sendOptimisticManifestToExperienceActivity(optimisticManifest) } 685 } 686 687 override fun onManifestCompleted(manifest: RawManifest) { 688 Exponent.getInstance().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.getInstance().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.getInstance().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 val opts = JSONObject() 735 if (existingTask == null) { 736 sendManifestToExperienceActivity(manifestUrl, manifest, bundleUrl, opts) 737 } 738 val params = Arguments.createMap().apply { 739 putString("manifestUrl", manifestUrl) 740 putString("manifestString", manifest.toString()) 741 } 742 queueEvent( 743 "ExponentKernel.addHistoryItem", params, 744 object : KernelEventCallback { 745 override fun onEventSuccess(result: ReadableMap) { 746 EXL.d(TAG, "Successfully called ExponentKernel.addHistoryItem in kernel JS.") 747 } 748 749 override fun onEventFailure(errorMessage: String?) { 750 EXL.e(TAG, "Error calling ExponentKernel.addHistoryItem in kernel JS: $errorMessage") 751 } 752 } 753 ) 754 killOrphanedLauncherActivities() 755 } 756 757 /* 758 * 759 * Optimistic experiences 760 * 761 */ 762 private fun openOptimisticExperienceActivity(manifestUrl: String?) { 763 try { 764 val intent = Intent(activityContext, ExperienceActivity::class.java).apply { 765 addIntentDocumentFlags(this) 766 putExtra(KernelConstants.MANIFEST_URL_KEY, manifestUrl) 767 putExtra(KernelConstants.IS_OPTIMISTIC_KEY, true) 768 } 769 activityContext!!.startActivity(intent) 770 } catch (e: Throwable) { 771 EXL.e(TAG, e) 772 } 773 } 774 775 fun setOptimisticActivity(experienceActivity: ExperienceActivity, taskId: Int) { 776 optimisticActivity = experienceActivity 777 optimisticTaskId = taskId 778 AsyncCondition.notify(KernelConstants.OPEN_OPTIMISTIC_EXPERIENCE_ACTIVITY_KEY) 779 AsyncCondition.notify(KernelConstants.OPEN_EXPERIENCE_ACTIVITY_KEY) 780 } 781 782 fun sendOptimisticManifestToExperienceActivity(optimisticManifest: RawManifest?) { 783 AsyncCondition.wait( 784 KernelConstants.OPEN_OPTIMISTIC_EXPERIENCE_ACTIVITY_KEY, 785 object : AsyncConditionListener { 786 override fun isReady(): Boolean { 787 return optimisticActivity != null && optimisticTaskId != null 788 } 789 790 override fun execute() { 791 optimisticActivity!!.setOptimisticManifest(optimisticManifest) 792 } 793 } 794 ) 795 } 796 797 private fun sendManifestToExperienceActivity( 798 manifestUrl: String?, 799 manifest: RawManifest?, 800 bundleUrl: String?, 801 kernelOptions: JSONObject? 802 ) { 803 AsyncCondition.wait( 804 KernelConstants.OPEN_EXPERIENCE_ACTIVITY_KEY, 805 object : AsyncConditionListener { 806 override fun isReady(): Boolean { 807 return optimisticActivity != null && optimisticTaskId != null 808 } 809 810 override fun execute() { 811 optimisticActivity!!.setManifest(manifestUrl, manifest, bundleUrl, kernelOptions) 812 AsyncCondition.notify(KernelConstants.LOAD_BUNDLE_FOR_EXPERIENCE_ACTIVITY_KEY) 813 } 814 } 815 ) 816 } 817 818 fun sendBundleToExperienceActivity(localBundlePath: String?) { 819 AsyncCondition.wait( 820 KernelConstants.LOAD_BUNDLE_FOR_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!!.setBundle(localBundlePath) 828 optimisticActivity = null 829 optimisticTaskId = null 830 } 831 } 832 ) 833 } 834 835 /* 836 * 837 * Tasks 838 * 839 */ 840 val tasks: List<AppTask> 841 get() { 842 val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 843 return manager.appTasks 844 } 845 846 // Get list of tasks in our format. 847 val experienceActivityTasks: List<AppTask> 848 get() = tasks 849 850 // Sometimes LauncherActivity.finish() doesn't close the activity and task. Not sure why exactly. 851 // Thought it was related to launchMode="singleTask" but other launchModes seem to have the same problem. 852 // This can be reproduced by creating a shortcut, exiting app, clicking on shortcut, refreshing, pressing 853 // home, clicking on shortcut, click recent apps button. There will be a blank LauncherActivity behind 854 // the ExperienceActivity. killOrphanedLauncherActivities solves this but would be nice to figure out 855 // the root cause. 856 private fun killOrphanedLauncherActivities() { 857 try { 858 // Crash with NoSuchFieldException instead of hard crashing at taskInfo.numActivities 859 RecentTaskInfo::class.java.getDeclaredField("numActivities") 860 for (task: AppTask in tasks) { 861 val taskInfo = task.taskInfo 862 if (taskInfo.numActivities == 0 && (taskInfo.baseIntent.action == Intent.ACTION_MAIN)) { 863 task.finishAndRemoveTask() 864 return 865 } 866 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 867 if (taskInfo.numActivities == 1 && (taskInfo.topActivity!!.className == LauncherActivity::class.java.name)) { 868 task.finishAndRemoveTask() 869 return 870 } 871 } 872 } 873 } catch (e: NoSuchFieldException) { 874 // Don't EXL here because this isn't actually a problem 875 Log.e(TAG, e.toString()) 876 } catch (e: Throwable) { 877 EXL.e(TAG, e) 878 } 879 } 880 881 fun moveTaskToFront(taskId: Int) { 882 tasks.find { it.taskInfo.id == taskId }?.also { task -> 883 // If we have the task in memory, tell the ExperienceActivity to check for new options. 884 // Otherwise options will be added in initialProps when the Experience starts. 885 val exponentTask = experienceActivityTaskForTaskId(taskId) 886 if (exponentTask != null) { 887 val experienceActivity = exponentTask.experienceActivity!!.get() 888 experienceActivity?.shouldCheckOptions() 889 } 890 task.moveToFront() 891 } 892 } 893 894 fun killActivityStack(activity: Activity) { 895 val exponentTask = experienceActivityTaskForTaskId(activity.taskId) 896 if (exponentTask != null) { 897 removeExperienceActivityTask(exponentTask.manifestUrl) 898 } 899 900 // Kill the current task. 901 val manager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 902 manager.appTasks.find { it.taskInfo.id == activity.taskId }?.also { task -> task.finishAndRemoveTask() } 903 } 904 905 override fun reloadVisibleExperience(manifestUrl: String, forceCache: Boolean): Boolean { 906 var activity: ExperienceActivity? = null 907 for (experienceActivityTask: ExperienceActivityTask in manifestUrlToExperienceActivityTask.values) { 908 if (manifestUrl == experienceActivityTask.manifestUrl) { 909 val weakActivity = 910 if (experienceActivityTask.experienceActivity == null) { 911 null 912 } else { 913 experienceActivityTask.experienceActivity!!.get() 914 } 915 activity = weakActivity 916 if (weakActivity == null) { 917 // No activity, just force a reload 918 break 919 } 920 Exponent.getInstance().runOnUiThread { weakActivity.startLoading() } 921 break 922 } 923 } 924 activity?.let { killActivityStack(it) } 925 openManifestUrl(manifestUrl, null, true, forceCache) 926 return true 927 } 928 929 override fun handleError(errorMessage: String) { 930 handleReactNativeError(developerErrorMessage(errorMessage), null, -1, true) 931 } 932 933 override fun handleError(exception: Exception) { 934 handleReactNativeError(ExceptionUtils.exceptionToErrorMessage(exception), null, -1, true) 935 } 936 937 // TODO: probably need to call this from other places. 938 fun setHasError() { 939 hasError = true 940 } 941 942 companion object { 943 private val TAG = Kernel::class.java.simpleName 944 private lateinit var instance: Kernel 945 946 // Activities/Tasks 947 private val manifestUrlToExperienceActivityTask = mutableMapOf<String, ExperienceActivityTask>() 948 private val manifestUrlToOptions = mutableMapOf<String?, ExperienceOptions>() 949 private val manifestUrlToAppLoader = mutableMapOf<String?, ExpoUpdatesAppLoader>() 950 951 private fun addIntentDocumentFlags(intent: Intent) = intent.apply { 952 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 953 addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT) 954 addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) 955 } 956 957 @JvmStatic 958 @DoNotStrip 959 fun reloadVisibleExperience(activityId: Int) { 960 val manifestUrl = getManifestUrlForActivityId(activityId) 961 if (manifestUrl != null) { 962 instance.reloadVisibleExperience(manifestUrl, false) 963 } 964 } 965 966 // Called from DevServerHelper via ReactNativeStaticHelpers 967 @JvmStatic 968 @DoNotStrip 969 fun getManifestUrlForActivityId(activityId: Int): String? { 970 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.manifestUrl 971 } 972 973 // Called from DevServerHelper via ReactNativeStaticHelpers 974 @JvmStatic 975 @DoNotStrip 976 fun getBundleUrlForActivityId( 977 activityId: Int, 978 host: String, 979 mainModuleId: String?, 980 bundleTypeId: String?, 981 devMode: Boolean, 982 jsMinify: Boolean 983 ): String? { 984 // NOTE: This current implementation doesn't look at the bundleTypeId (see RN's private 985 // BundleType enum for the possible values) but may need to 986 if (activityId == -1) { 987 // This is the kernel 988 return instance.bundleUrl 989 } 990 if (InternalHeadlessAppLoader.hasBundleUrlForActivityId(activityId)) { 991 return InternalHeadlessAppLoader.getBundleUrlForActivityId(activityId) 992 } 993 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.bundleUrl 994 } 995 996 // <= SDK 25 997 @DoNotStrip 998 fun getBundleUrlForActivityId( 999 activityId: Int, 1000 host: String, 1001 jsModulePath: String?, 1002 devMode: Boolean, 1003 jsMinify: Boolean 1004 ): String? { 1005 if (activityId == -1) { 1006 // This is the kernel 1007 return instance.bundleUrl 1008 } 1009 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.bundleUrl 1010 } 1011 1012 // <= SDK 21 1013 @DoNotStrip 1014 fun getBundleUrlForActivityId( 1015 activityId: Int, 1016 host: String, 1017 jsModulePath: String?, 1018 devMode: Boolean, 1019 hmr: Boolean, 1020 jsMinify: Boolean 1021 ): String? { 1022 if (activityId == -1) { 1023 // This is the kernel 1024 return instance.bundleUrl 1025 } 1026 return manifestUrlToExperienceActivityTask.values.find { it.activityId == activityId }?.let { task -> 1027 var url = task.bundleUrl ?: return null 1028 if (hmr) { 1029 url = if (url.contains("hot=false")) { 1030 url.replace("hot=false", "hot=true") 1031 } else { 1032 "$url&hot=true" 1033 } 1034 } 1035 return url 1036 } 1037 } 1038 1039 /* 1040 * 1041 * Error handling 1042 * 1043 */ 1044 // Called using reflection from ReactAndroid. 1045 @DoNotStrip 1046 fun handleReactNativeError( 1047 errorMessage: String?, 1048 detailsUnversioned: Any?, 1049 exceptionId: Int?, 1050 isFatal: Boolean 1051 ) { 1052 handleReactNativeError( 1053 developerErrorMessage(errorMessage), 1054 detailsUnversioned, 1055 exceptionId, 1056 isFatal 1057 ) 1058 } 1059 1060 // Called using reflection from ReactAndroid. 1061 @DoNotStrip 1062 fun handleReactNativeError( 1063 throwable: Throwable?, 1064 errorMessage: String?, 1065 detailsUnversioned: Any?, 1066 exceptionId: Int?, 1067 isFatal: Boolean 1068 ) { 1069 handleReactNativeError( 1070 developerErrorMessage(errorMessage), 1071 detailsUnversioned, 1072 exceptionId, 1073 isFatal 1074 ) 1075 } 1076 1077 private fun handleReactNativeError( 1078 errorMessage: ExponentErrorMessage, 1079 detailsUnversioned: Any?, 1080 exceptionId: Int?, 1081 isFatal: Boolean 1082 ) { 1083 val stackList = ArrayList<Bundle>() 1084 if (detailsUnversioned != null) { 1085 val details = RNObject.wrap(detailsUnversioned) 1086 val arguments = RNObject("com.facebook.react.bridge.Arguments") 1087 arguments.loadVersion(details.version()) 1088 for (i in 0 until details.call("size") as Int) { 1089 try { 1090 val bundle = arguments.callStatic("toBundle", details.call("getMap", i)) as Bundle 1091 stackList.add(bundle) 1092 } catch (e: Exception) { 1093 e.printStackTrace() 1094 } 1095 } 1096 } else if (BuildConfig.DEBUG) { 1097 val stackTraceElements = Thread.currentThread().stackTrace 1098 // stackTraceElements starts with a bunch of stuff we don't care about. 1099 for (i in 2 until stackTraceElements.size) { 1100 val element = stackTraceElements[i] 1101 if (( 1102 (element.fileName != null) && element.fileName.startsWith(Kernel::class.java.simpleName) && 1103 ((element.methodName == "handleReactNativeError") || (element.methodName == "handleError")) 1104 ) 1105 ) { 1106 // Ignore these base error handling methods. 1107 continue 1108 } 1109 val bundle = Bundle().apply { 1110 putInt("column", 0) 1111 putInt("lineNumber", element.lineNumber) 1112 putString("methodName", element.methodName) 1113 putString("file", element.fileName) 1114 } 1115 stackList.add(bundle) 1116 } 1117 } 1118 val stack = stackList.toTypedArray() 1119 BaseExperienceActivity.addError( 1120 ExponentError( 1121 errorMessage, stack, 1122 getExceptionId(exceptionId), isFatal 1123 ) 1124 ) 1125 } 1126 1127 private fun getExceptionId(originalId: Int?): Int { 1128 return if (originalId == null || originalId == -1) { 1129 (-(Math.random() * Int.MAX_VALUE)).toInt() 1130 } else originalId 1131 } 1132 } 1133 1134 init { 1135 NativeModuleDepsProvider.getInstance().inject(Kernel::class.java, this) 1136 instance = this 1137 updateKernelRNOkHttp() 1138 } 1139 } 1140