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