1 // Copyright 2015-present 650 Industries. All rights reserved. 2 package host.exp.exponent.experience 3 4 import android.app.AlertDialog 5 import android.app.Notification 6 import android.app.NotificationManager 7 import android.app.PendingIntent 8 import android.content.Context 9 import android.content.Intent 10 import android.net.Uri 11 import android.os.Build 12 import android.os.Bundle 13 import android.text.TextUtils 14 import android.view.KeyEvent 15 import android.view.View 16 import android.view.ViewGroup 17 import android.view.animation.AccelerateInterpolator 18 import android.view.animation.AlphaAnimation 19 import android.view.animation.Animation 20 import android.widget.RemoteViews 21 import androidx.core.app.NotificationCompat 22 import androidx.core.content.ContextCompat 23 import com.facebook.react.ReactPackage 24 import com.facebook.react.bridge.UiThreadUtil 25 import com.facebook.soloader.SoLoader 26 import com.google.firebase.crashlytics.FirebaseCrashlytics 27 import de.greenrobot.event.EventBus 28 import expo.modules.core.interfaces.Package 29 import expo.modules.manifests.core.Manifest 30 import expo.modules.splashscreen.singletons.SplashScreen 31 import host.exp.exponent.* 32 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderCallback 33 import host.exp.exponent.ExpoUpdatesAppLoader.AppLoaderStatus 34 import host.exp.exponent.analytics.Analytics 35 import host.exp.exponent.analytics.EXL 36 import host.exp.exponent.branch.BranchManager 37 import host.exp.exponent.di.NativeModuleDepsProvider 38 import host.exp.exponent.experience.loading.LoadingProgressPopupController 39 import host.exp.exponent.experience.splashscreen.ManagedAppSplashScreenConfiguration 40 import host.exp.exponent.experience.splashscreen.ManagedAppSplashScreenViewController 41 import host.exp.exponent.experience.splashscreen.ManagedAppSplashScreenViewProvider 42 import host.exp.exponent.kernel.* 43 import host.exp.exponent.kernel.Kernel.KernelStartedRunningEvent 44 import host.exp.exponent.kernel.KernelConstants.ExperienceOptions 45 import host.exp.exponent.notifications.* 46 import host.exp.exponent.storage.ExponentDB 47 import host.exp.exponent.storage.ExponentDBObject 48 import host.exp.exponent.utils.AsyncCondition 49 import host.exp.exponent.utils.AsyncCondition.AsyncConditionListener 50 import host.exp.exponent.utils.ExperienceActivityUtils 51 import host.exp.exponent.utils.ExperienceRTLManager 52 import host.exp.exponent.utils.ExpoActivityIds 53 import host.exp.expoview.Exponent 54 import host.exp.expoview.Exponent.StartReactInstanceDelegate 55 import host.exp.expoview.R 56 import org.json.JSONArray 57 import org.json.JSONException 58 import org.json.JSONObject 59 import versioned.host.exp.exponent.ExponentPackageDelegate 60 import versioned.host.exp.exponent.ReactUnthemedRootView 61 import java.lang.ref.WeakReference 62 import javax.inject.Inject 63 64 open class ExperienceActivity : BaseExperienceActivity(), StartReactInstanceDelegate { 65 open fun expoPackages(): List<Package>? { 66 // Experience must pick its own modules in ExponentPackage 67 return null 68 } 69 70 open fun reactPackages(): List<ReactPackage>? { 71 return null 72 } 73 74 override val exponentPackageDelegate: ExponentPackageDelegate? = null 75 76 private var nuxOverlayView: ReactUnthemedRootView? = null 77 private var notification: ExponentNotification? = null 78 private var tempNotification: ExponentNotification? = null 79 private var isShellApp = false 80 protected var intentUri: String? = null 81 private var isReadyForBundle = false 82 private var notificationRemoteViews: RemoteViews? = null 83 private var notificationBuilder: NotificationCompat.Builder? = null 84 private var isLoadExperienceAllowedToRun = false 85 private var shouldShowLoadingViewWithOptimisticManifest = false 86 87 /** 88 * Controls loadingProgressPopupWindow that is shown above whole activity. 89 */ 90 lateinit var loadingProgressPopupController: LoadingProgressPopupController 91 var managedAppSplashScreenViewProvider: ManagedAppSplashScreenViewProvider? = null 92 var managedAppSplashScreenViewController: ManagedAppSplashScreenViewController? = null 93 94 @Inject 95 lateinit var exponentManifest: ExponentManifest 96 97 @Inject 98 lateinit var devMenuManager: DevMenuManager 99 100 private val devBundleDownloadProgressListener: DevBundleDownloadProgressListener = 101 object : DevBundleDownloadProgressListener { 102 override fun onProgress(status: String?, done: Int?, total: Int?) { 103 UiThreadUtil.runOnUiThread { 104 loadingProgressPopupController.updateProgress( 105 status, 106 done, 107 total 108 ) 109 } 110 } 111 112 override fun onSuccess() { 113 UiThreadUtil.runOnUiThread { 114 loadingProgressPopupController.hide() 115 managedAppSplashScreenViewController?.startSplashScreenWarningTimer() 116 finishLoading() 117 } 118 } 119 120 override fun onFailure(error: Exception) { 121 UiThreadUtil.runOnUiThread { 122 loadingProgressPopupController.hide() 123 interruptLoading() 124 } 125 } 126 } 127 128 /* 129 * 130 * Lifecycle 131 * 132 */ 133 override fun onCreate(savedInstanceState: Bundle?) { 134 super.onCreate(savedInstanceState) 135 136 isLoadExperienceAllowedToRun = true 137 shouldShowLoadingViewWithOptimisticManifest = true 138 loadingProgressPopupController = LoadingProgressPopupController(this) 139 140 NativeModuleDepsProvider.instance.inject(ExperienceActivity::class.java, this) 141 EventBus.getDefault().registerSticky(this) 142 143 activityId = ExpoActivityIds.getNextAppActivityId() 144 145 // TODO: audit this now that kernel logic is on the native side in Kotlin 146 var shouldOpenImmediately = true 147 148 // If our activity was killed for memory reasons or because of "Don't keep activities", 149 // try to reload manifest using the savedInstanceState 150 if (savedInstanceState != null) { 151 val manifestUrl = savedInstanceState.getString(KernelConstants.MANIFEST_URL_KEY) 152 if (manifestUrl != null) { 153 this.manifestUrl = manifestUrl 154 } 155 } 156 157 // On cold boot to experience, we're given this information from the Kotlin kernel, instead of 158 // the JS kernel. 159 val bundle = intent.extras 160 if (bundle != null && this.manifestUrl == null) { 161 val manifestUrl = bundle.getString(KernelConstants.MANIFEST_URL_KEY) 162 if (manifestUrl != null) { 163 this.manifestUrl = manifestUrl 164 } 165 166 // Don't want to get here if savedInstanceState has manifestUrl. Only care about 167 // IS_OPTIMISTIC_KEY the first time onCreate is called. 168 val isOptimistic = bundle.getBoolean(KernelConstants.IS_OPTIMISTIC_KEY) 169 if (isOptimistic) { 170 shouldOpenImmediately = false 171 } 172 } 173 174 FirebaseCrashlytics.getInstance().log("ExperienceActivity.manifestUrl: ${this.manifestUrl}") 175 if (this.manifestUrl != null && shouldOpenImmediately) { 176 val forceCache = intent.getBooleanExtra(KernelConstants.LOAD_FROM_CACHE_KEY, false) 177 ExpoUpdatesAppLoader( 178 this.manifestUrl!!, 179 object : AppLoaderCallback { 180 override fun onOptimisticManifest(optimisticManifest: Manifest) { 181 Exponent.instance.runOnUiThread { setOptimisticManifest(optimisticManifest) } 182 } 183 184 override fun onManifestCompleted(manifest: Manifest) { 185 Exponent.instance.runOnUiThread { 186 try { 187 val bundleUrl = ExponentUrls.toHttp(manifest.getBundleURL()) 188 setManifest(this@ExperienceActivity.manifestUrl!!, manifest, bundleUrl) 189 } catch (e: JSONException) { 190 kernel.handleError(e) 191 } 192 } 193 } 194 195 override fun onBundleCompleted(localBundlePath: String) { 196 Exponent.instance.runOnUiThread { setBundle(localBundlePath) } 197 } 198 199 override fun emitEvent(params: JSONObject) { 200 emitUpdatesEvent(params) 201 } 202 203 override fun updateStatus(status: AppLoaderStatus) { 204 setLoadingProgressStatusIfEnabled(status) 205 } 206 207 override fun onError(e: Exception) { 208 Exponent.instance.runOnUiThread { kernel.handleError(e) } 209 } 210 }, 211 forceCache 212 ).start(this) 213 } 214 kernel.setOptimisticActivity(this, taskId) 215 } 216 217 override fun onResume() { 218 super.onResume() 219 currentActivity = this 220 221 // Resume home's host if needed. 222 devMenuManager.maybeResumeHostWithActivity(this) 223 224 soLoaderInit() 225 226 addNotification() 227 Analytics.logEventWithManifestUrl(Analytics.AnalyticsEvent.EXPERIENCE_APPEARED, manifestUrl) 228 } 229 230 override fun onWindowFocusChanged(hasFocus: Boolean) { 231 super.onWindowFocusChanged(hasFocus) 232 // Check for manifest to avoid calling this when first loading an experience 233 if (hasFocus && manifest != null) { 234 runOnUiThread { ExperienceActivityUtils.setNavigationBar(manifest!!, this@ExperienceActivity) } 235 } 236 } 237 238 private fun soLoaderInit() { 239 if (detachSdkVersion != null) { 240 SoLoader.init(this, false) 241 } 242 } 243 244 open fun shouldCheckOptions() { 245 if (manifestUrl != null && kernel.hasOptionsForManifestUrl(manifestUrl)) { 246 handleOptions(kernel.popOptionsForManifestUrl(manifestUrl)!!) 247 } 248 } 249 250 override fun onPause() { 251 super.onPause() 252 if (currentActivity === this) { 253 currentActivity = null 254 } 255 removeNotification() 256 Analytics.clearTimedEvents() 257 } 258 259 public override fun onSaveInstanceState(savedInstanceState: Bundle) { 260 savedInstanceState.putString(KernelConstants.MANIFEST_URL_KEY, manifestUrl) 261 super.onSaveInstanceState(savedInstanceState) 262 } 263 264 override fun onNewIntent(intent: Intent) { 265 super.onNewIntent(intent) 266 val uri = intent.data 267 if (uri != null) { 268 handleUri(uri.toString()) 269 } 270 } 271 272 fun toggleDevMenu(): Boolean { 273 if (reactInstanceManager.isNotNull && !isCrashed) { 274 devMenuManager.toggleInActivity(this) 275 return true 276 } 277 return false 278 } 279 280 /** 281 * Handles command line command `adb shell input keyevent 82` that toggles the dev menu on the current experience activity. 282 */ 283 override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { 284 if (keyCode == KeyEvent.KEYCODE_MENU && reactInstanceManager.isNotNull && !isCrashed) { 285 devMenuManager.toggleInActivity(this) 286 return true 287 } 288 return super.onKeyUp(keyCode, event) 289 } 290 291 /** 292 * Closes the dev menu when pressing back button when it is visible on this activity. 293 */ 294 override fun onBackPressed() { 295 if (currentActivity === this && devMenuManager.isShownInActivity(this)) { 296 devMenuManager.requestToClose(this) 297 return 298 } 299 super.onBackPressed() 300 } 301 302 fun onEventMainThread(event: KernelStartedRunningEvent?) { 303 AsyncCondition.notify(KERNEL_STARTED_RUNNING_KEY) 304 } 305 306 override fun onDoneLoading() { 307 Analytics.markEvent(Analytics.TimedEvent.FINISHED_LOADING_REACT_NATIVE) 308 Analytics.sendTimedEvents(manifestUrl) 309 } 310 311 fun onEvent(event: ExperienceDoneLoadingEvent) { 312 if (event.activity === this) { 313 loadingProgressPopupController.hide() 314 } 315 316 if (!Constants.isStandaloneApp()) { 317 val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl) 318 if (appLoader != null && !appLoader.isUpToDate && appLoader.shouldShowAppLoaderStatus) { 319 AlertDialog.Builder(this@ExperienceActivity) 320 .setTitle("Using a cached project") 321 .setMessage("Expo was unable to fetch the latest update to this app. A previously downloaded version has been launched. If you did not intend to use a cached project, check your network connection and reload the app.") 322 .setPositiveButton("Use cache", null) 323 .setNegativeButton("Reload") { _, _ -> 324 kernel.reloadVisibleExperience( 325 manifestUrl!!, false 326 ) 327 } 328 .show() 329 } 330 } 331 } 332 333 /* 334 * 335 * Experience Loading 336 * 337 */ 338 fun startLoading() { 339 isLoading = true 340 showOrReconfigureManagedAppSplashScreen(manifest) 341 setLoadingProgressStatusIfEnabled() 342 } 343 344 /** 345 * This method is being called twice: 346 * - first time for optimistic manifest 347 * - seconds time for real manifest 348 */ 349 protected fun showOrReconfigureManagedAppSplashScreen(manifest: Manifest?) { 350 if (!shouldCreateLoadingView()) { 351 return 352 } 353 354 hideLoadingView() 355 if (managedAppSplashScreenViewProvider == null) { 356 val config = ManagedAppSplashScreenConfiguration.parseManifest( 357 manifest!! 358 ) 359 managedAppSplashScreenViewProvider = ManagedAppSplashScreenViewProvider(config) 360 val splashScreenView = managedAppSplashScreenViewProvider!!.createSplashScreenView(this) 361 managedAppSplashScreenViewController = ManagedAppSplashScreenViewController( 362 this, 363 getRootViewClass( 364 manifest 365 ), 366 splashScreenView 367 ) 368 SplashScreen.show(this, managedAppSplashScreenViewController!!, true) 369 } else { 370 managedAppSplashScreenViewProvider!!.updateSplashScreenViewWithManifest(this, manifest!!) 371 } 372 } 373 374 fun setLoadingProgressStatusIfEnabled() { 375 val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl) 376 if (appLoader != null) { 377 setLoadingProgressStatusIfEnabled(appLoader.status) 378 } 379 } 380 381 fun setLoadingProgressStatusIfEnabled(status: AppLoaderStatus?) { 382 if (Constants.isStandaloneApp()) { 383 return 384 } 385 if (status == null) { 386 return 387 } 388 val appLoader = kernel.getAppLoaderForManifestUrl(manifestUrl) 389 if (appLoader != null && appLoader.shouldShowAppLoaderStatus) { 390 UiThreadUtil.runOnUiThread { loadingProgressPopupController.setLoadingProgressStatus(status) } 391 } else { 392 UiThreadUtil.runOnUiThread { loadingProgressPopupController.hide() } 393 } 394 } 395 396 fun setOptimisticManifest(optimisticManifest: Manifest) { 397 runOnUiThread { 398 if (!isInForeground) { 399 return@runOnUiThread 400 } 401 if (!shouldShowLoadingViewWithOptimisticManifest) { 402 return@runOnUiThread 403 } 404 ExperienceActivityUtils.configureStatusBar(optimisticManifest, this@ExperienceActivity) 405 ExperienceActivityUtils.setNavigationBar(optimisticManifest, this@ExperienceActivity) 406 ExperienceActivityUtils.setTaskDescription( 407 exponentManifest, 408 optimisticManifest, 409 this@ExperienceActivity 410 ) 411 showOrReconfigureManagedAppSplashScreen(optimisticManifest) 412 setLoadingProgressStatusIfEnabled() 413 ExperienceRTLManager.setSupportsRTLFromManifest(this, optimisticManifest) 414 } 415 } 416 417 fun setManifest( 418 manifestUrl: String, 419 manifest: Manifest, 420 bundleUrl: String 421 ) { 422 if (!isInForeground) { 423 return 424 } 425 if (!isLoadExperienceAllowedToRun) { 426 return 427 } 428 429 // Only want to run once per onCreate. There are some instances with ShellAppActivity where this would be called 430 // twice otherwise. Turn on "Don't keep activities", trigger a notification, background the app, and then 431 // press on the notification in a shell app to see this happen. 432 isLoadExperienceAllowedToRun = false 433 434 isReadyForBundle = false 435 this.manifestUrl = manifestUrl 436 this.manifest = manifest 437 438 exponentSharedPreferences.removeLegacyManifest(this.manifestUrl!!) 439 440 // Notifications logic uses this to determine which experience to route a notification to 441 ExponentDB.saveExperience(ExponentDBObject(this.manifestUrl!!, manifest, bundleUrl)) 442 443 ExponentNotificationManager(this).maybeCreateNotificationChannelGroup(this.manifest!!) 444 445 val task = kernel.getExperienceActivityTask(this.manifestUrl!!) 446 task.taskId = taskId 447 task.experienceActivity = WeakReference(this) 448 task.activityId = activityId 449 task.bundleUrl = bundleUrl 450 451 sdkVersion = manifest.getSDKVersion() 452 isShellApp = this.manifestUrl == Constants.INITIAL_URL 453 454 // Sometime we want to release a new version without adding a new .aar. Use TEMPORARY_ABI_VERSION 455 // to point to the unversioned code in ReactAndroid. 456 if (Constants.TEMPORARY_ABI_VERSION != null && Constants.TEMPORARY_ABI_VERSION == sdkVersion) { 457 sdkVersion = RNObject.UNVERSIONED 458 } 459 460 // In detach/shell, we always use UNVERSIONED as the ABI. 461 detachSdkVersion = if (Constants.isStandaloneApp()) RNObject.UNVERSIONED else sdkVersion 462 463 if (RNObject.UNVERSIONED != sdkVersion) { 464 var isValidVersion = false 465 for (version in Constants.SDK_VERSIONS_LIST) { 466 if (version == sdkVersion) { 467 isValidVersion = true 468 break 469 } 470 } 471 if (!isValidVersion) { 472 KernelProvider.instance.handleError( 473 sdkVersion + " is not a valid SDK version. Options are " + 474 TextUtils.join(", ", Constants.SDK_VERSIONS_LIST) + ", " + RNObject.UNVERSIONED + "." 475 ) 476 return 477 } 478 } 479 480 soLoaderInit() 481 482 try { 483 experienceKey = ExperienceKey.fromManifest(manifest) 484 AsyncCondition.notify(KernelConstants.EXPERIENCE_ID_SET_FOR_ACTIVITY_KEY) 485 } catch (e: JSONException) { 486 KernelProvider.instance.handleError("No ID found in manifest.") 487 return 488 } 489 490 isCrashed = false 491 492 Analytics.logEventWithManifestUrlSdkVersion(Analytics.AnalyticsEvent.LOAD_EXPERIENCE, manifestUrl, sdkVersion) 493 494 ExperienceActivityUtils.updateOrientation(this.manifest!!, this) 495 ExperienceActivityUtils.updateSoftwareKeyboardLayoutMode(this.manifest!!, this) 496 ExperienceActivityUtils.overrideUiMode(this.manifest!!, this) 497 498 addNotification() 499 500 var notificationObject: ExponentNotification? = null 501 // Activity could be restarted due to Dark Mode change, only pop options if that will not happen 502 if (kernel.hasOptionsForManifestUrl(manifestUrl)) { 503 val options = kernel.popOptionsForManifestUrl(manifestUrl) 504 505 // if the kernel has an intent for our manifest url, that's the intent that triggered 506 // the loading of this experience. 507 if (options!!.uri != null) { 508 intentUri = options.uri 509 } 510 notificationObject = options.notificationObject 511 } 512 513 BranchManager.handleLink(this, intentUri, detachSdkVersion) 514 515 ExperienceRTLManager.setSupportsRTLFromManifest(this, manifest) 516 517 runOnUiThread { 518 if (!isInForeground) { 519 return@runOnUiThread 520 } 521 if (reactInstanceManager.isNotNull) { 522 reactInstanceManager.onHostDestroy() 523 reactInstanceManager.assign(null) 524 } 525 526 reactRootView = RNObject("host.exp.exponent.ReactUnthemedRootView") 527 reactRootView.loadVersion(detachSdkVersion!!).construct(this@ExperienceActivity) 528 setReactRootView((reactRootView.get() as View)) 529 530 if (isDebugModeEnabled) { 531 notification = notificationObject 532 jsBundlePath = "" 533 startReactInstance() 534 } else { 535 tempNotification = notificationObject 536 isReadyForBundle = true 537 AsyncCondition.notify(READY_FOR_BUNDLE) 538 } 539 540 ExperienceActivityUtils.configureStatusBar(manifest, this@ExperienceActivity) 541 ExperienceActivityUtils.setNavigationBar(manifest, this@ExperienceActivity) 542 ExperienceActivityUtils.setTaskDescription( 543 exponentManifest, 544 manifest, 545 this@ExperienceActivity 546 ) 547 showOrReconfigureManagedAppSplashScreen(manifest) 548 setLoadingProgressStatusIfEnabled() 549 } 550 } 551 552 fun setBundle(localBundlePath: String) { 553 // by this point, setManifest should have also been called, so prevent 554 // setOptimisticManifest from showing a rogue splash screen 555 shouldShowLoadingViewWithOptimisticManifest = false 556 if (!isDebugModeEnabled) { 557 val finalIsReadyForBundle = isReadyForBundle 558 AsyncCondition.wait( 559 READY_FOR_BUNDLE, 560 object : AsyncConditionListener { 561 override fun isReady(): Boolean { 562 return finalIsReadyForBundle 563 } 564 565 override fun execute() { 566 notification = tempNotification 567 tempNotification = null 568 jsBundlePath = localBundlePath 569 startReactInstance() 570 AsyncCondition.remove(READY_FOR_BUNDLE) 571 } 572 } 573 ) 574 } 575 } 576 577 fun onEventMainThread(event: ReceivedNotificationEvent) { 578 // TODO(wschurman): investigate removal, this probably is no longer used 579 if (experienceKey != null && event.experienceScopeKey == experienceKey!!.scopeKey) { 580 try { 581 val rctDeviceEventEmitter = 582 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 583 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 584 reactInstanceManager.callRecursive("getCurrentReactContext")!! 585 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!! 586 .call("emit", "Exponent.notification", event.toWriteableMap(detachSdkVersion, "received")) 587 } catch (e: Throwable) { 588 EXL.e(TAG, e) 589 } 590 } 591 } 592 593 fun handleOptions(options: ExperienceOptions) { 594 try { 595 val uri = options.uri 596 if (uri !== null) { 597 handleUri(uri) 598 val rctDeviceEventEmitter = 599 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 600 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 601 reactInstanceManager.callRecursive("getCurrentReactContext")!! 602 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!! 603 .call("emit", "Exponent.openUri", uri) 604 BranchManager.handleLink(this, uri, detachSdkVersion) 605 } 606 if ((options.notification != null || options.notificationObject != null) && detachSdkVersion != null) { 607 val rctDeviceEventEmitter = 608 RNObject("com.facebook.react.modules.core.DeviceEventManagerModule\$RCTDeviceEventEmitter") 609 rctDeviceEventEmitter.loadVersion(detachSdkVersion!!) 610 reactInstanceManager.callRecursive("getCurrentReactContext")!! 611 .callRecursive("getJSModule", rctDeviceEventEmitter.rnClass())!! 612 .call( 613 "emit", 614 "Exponent.notification", 615 options.notificationObject!!.toWriteableMap(detachSdkVersion, "selected") 616 ) 617 } 618 } catch (e: Throwable) { 619 EXL.e(TAG, e) 620 } 621 } 622 623 private fun handleUri(uri: String) { 624 // Emits a "url" event to the Linking event emitter 625 val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri)) 626 super.onNewIntent(intent) 627 } 628 629 fun emitUpdatesEvent(params: JSONObject) { 630 KernelProvider.instance.addEventForExperience( 631 manifestUrl!!, 632 KernelConstants.ExperienceEvent(ExpoUpdatesAppLoader.UPDATES_EVENT_NAME, params.toString()) 633 ) 634 } 635 636 override val isDebugModeEnabled: Boolean 637 get() = manifest?.isDevelopmentMode() ?: false 638 639 override fun startReactInstance() { 640 Exponent.instance 641 .testPackagerStatus( 642 isDebugModeEnabled, manifest!!, 643 object : Exponent.PackagerStatusCallback { 644 override fun onSuccess() { 645 reactInstanceManager = startReactInstance( 646 this@ExperienceActivity, 647 intentUri, 648 detachSdkVersion, 649 notification, 650 isShellApp, 651 reactPackages(), 652 expoPackages(), 653 devBundleDownloadProgressListener 654 ) 655 } 656 657 override fun onFailure(errorMessage: String) { 658 KernelProvider.instance.handleError(errorMessage) 659 } 660 } 661 ) 662 } 663 664 override fun handleUnreadNotifications(unreadNotifications: JSONArray) { 665 PushNotificationHelper.instance.removeNotifications(this, unreadNotifications) 666 } 667 668 /* 669 * 670 * Notification 671 * 672 */ 673 private fun addNotification() { 674 if (isShellApp || manifestUrl == null || manifest == null) { 675 return 676 } 677 678 val name = manifest!!.getName() ?: return 679 680 val remoteViews = RemoteViews(packageName, R.layout.notification) 681 remoteViews.setCharSequence(R.id.home_text_button, "setText", name) 682 683 // We're defaulting to the behaviour prior API 31 (mutable) even though Android recommends immutability 684 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 685 686 // Home 687 val homeIntent = Intent(this, LauncherActivity::class.java) 688 remoteViews.setOnClickPendingIntent( 689 R.id.home_image_button, 690 PendingIntent.getActivity( 691 this, 0, 692 homeIntent, mutableFlag 693 ) 694 ) 695 696 // Reload 697 remoteViews.setOnClickPendingIntent( 698 R.id.reload_button, 699 PendingIntent.getService( 700 this, 0, 701 ExponentIntentService.getActionReloadExperience(this, manifestUrl!!), PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag 702 ) 703 ) 704 705 notificationRemoteViews = remoteViews 706 707 // Build the actual notification 708 val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager 709 notificationManager.cancel(PERSISTENT_EXPONENT_NOTIFICATION_ID) 710 711 ExponentNotificationManager(this).maybeCreateExpoPersistentNotificationChannel() 712 notificationBuilder = 713 NotificationCompat.Builder(this, NotificationConstants.NOTIFICATION_EXPERIENCE_CHANNEL_ID) 714 .setContent(notificationRemoteViews) 715 .setSmallIcon(R.drawable.notification_icon) 716 .setShowWhen(false) 717 .setOngoing(true) 718 .setPriority(Notification.PRIORITY_MAX) 719 .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) 720 721 notificationManager.notify(PERSISTENT_EXPONENT_NOTIFICATION_ID, notificationBuilder!!.build()) 722 } 723 724 fun removeNotification() { 725 notificationRemoteViews = null 726 notificationBuilder = null 727 removeNotification(this) 728 } 729 730 fun onNotificationAction() { 731 dismissNuxViewIfVisible(true) 732 } 733 734 /** 735 * @param isFromNotification true if this is the result of the user taking an 736 * action in the notification view. 737 */ 738 fun dismissNuxViewIfVisible(isFromNotification: Boolean) { 739 if (nuxOverlayView == null) { 740 return 741 } 742 743 runOnUiThread { 744 val fadeOut: Animation = AlphaAnimation(1f, 0f).apply { 745 interpolator = AccelerateInterpolator() 746 duration = 500 747 setAnimationListener(object : Animation.AnimationListener { 748 override fun onAnimationEnd(animation: Animation) { 749 if (nuxOverlayView!!.parent != null) { 750 (nuxOverlayView!!.parent as ViewGroup).removeView(nuxOverlayView) 751 } 752 nuxOverlayView = null 753 val eventProperties = JSONObject() 754 try { 755 eventProperties.put("IS_FROM_NOTIFICATION", isFromNotification) 756 } catch (e: JSONException) { 757 EXL.e(TAG, e.message) 758 } 759 Analytics.logEvent(Analytics.AnalyticsEvent.NUX_EXPERIENCE_OVERLAY_DISMISSED, eventProperties) 760 } 761 762 override fun onAnimationRepeat(animation: Animation) {} 763 override fun onAnimationStart(animation: Animation) {} 764 }) 765 } 766 nuxOverlayView!!.startAnimation(fadeOut) 767 } 768 } 769 770 /* 771 * 772 * Errors 773 * 774 */ 775 override fun onError(intent: Intent) { 776 if (manifestUrl != null) { 777 intent.putExtra(ErrorActivity.MANIFEST_URL_KEY, manifestUrl) 778 } 779 } 780 781 companion object { 782 private val TAG = ExperienceActivity::class.java.simpleName 783 private const val KERNEL_STARTED_RUNNING_KEY = "experienceActivityKernelDidLoad" 784 const val PERSISTENT_EXPONENT_NOTIFICATION_ID = 10101 785 private const val READY_FOR_BUNDLE = "readyForBundle" 786 787 /** 788 * Returns the currently active ExperienceActivity, that is the one that is currently being used by the user. 789 */ 790 var currentActivity: ExperienceActivity? = null 791 private set 792 793 @JvmStatic fun removeNotification(context: Context) { 794 val notificationManager = 795 context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager 796 notificationManager.cancel(PERSISTENT_EXPONENT_NOTIFICATION_ID) 797 } 798 } 799 } 800