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