react-native-firework-sdk 2.17.5 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/android/gradle.properties +1 -1
  2. package/android/src/main/java/com/fireworksdk/bridge/components/storyblock/StoryBlockContainerView.kt +436 -30
  3. package/android/src/main/java/com/fireworksdk/bridge/models/FWPlayerButtonConfigurationDeserializer.kt +3 -0
  4. package/android/src/main/java/com/fireworksdk/bridge/models/FWPlayerButtonConfigurationModel.kt +1 -0
  5. package/android/src/main/java/com/fireworksdk/bridge/models/FWPlayerButtonConfigurationSerializer.kt +2 -0
  6. package/android/src/main/java/com/fireworksdk/bridge/models/FWVideoPlayerConfigModel.kt +3 -0
  7. package/android/src/main/java/com/fireworksdk/bridge/models/FWVideoPlayerConfigModelDeserializer.kt +7 -0
  8. package/android/src/main/java/com/fireworksdk/bridge/models/FWVideoPlayerConfigModelSerializer.kt +5 -0
  9. package/android/src/main/java/com/fireworksdk/bridge/models/enums/FWVideoPlayerScrollDirection.kt +17 -0
  10. package/android/src/main/java/com/fireworksdk/bridge/models/enums/FWVideoPlayerVersion.kt +6 -0
  11. package/android/src/main/java/com/fireworksdk/bridge/reactnative/manager/FWVideoFeedManager.kt +2 -2
  12. package/android/src/main/java/com/fireworksdk/bridge/reactnative/module/FireworkSDKModule.kt +3 -2
  13. package/android/src/main/java/com/fireworksdk/bridge/reactnative/utils/FWEventUtils.kt +22 -0
  14. package/android/src/main/java/com/fireworksdk/bridge/utils/FWConfigUtil.kt +23 -2
  15. package/android/src/main/java/com/fireworksdk/bridge/utils/FWLanguageUtil.kt +11 -1
  16. package/ios/Components/StoryBlock.swift +15 -0
  17. package/ios/Components/StoryBlockConfiguration.swift +2 -0
  18. package/ios/Components/VideoFeed.swift +9 -0
  19. package/ios/Components/VideoPlayerConfiguration.swift +5 -0
  20. package/ios/Models/NativeToRN/FireworkSDK+Json.swift +33 -3
  21. package/lib/commonjs/components/StoryBlock.js +4 -0
  22. package/lib/commonjs/components/StoryBlock.js.map +1 -1
  23. package/lib/commonjs/components/VideoFeed.js +2 -0
  24. package/lib/commonjs/components/VideoFeed.js.map +1 -1
  25. package/lib/commonjs/index.js +14 -0
  26. package/lib/commonjs/index.js.map +1 -1
  27. package/lib/commonjs/models/LiveStreamStatus.js +32 -0
  28. package/lib/commonjs/models/LiveStreamStatus.js.map +1 -0
  29. package/lib/commonjs/models/ScrollDirection.js +2 -0
  30. package/lib/commonjs/models/ScrollDirection.js.map +1 -0
  31. package/lib/commonjs/models/VideoType.js +26 -0
  32. package/lib/commonjs/models/VideoType.js.map +1 -0
  33. package/lib/module/components/StoryBlock.js +4 -0
  34. package/lib/module/components/StoryBlock.js.map +1 -1
  35. package/lib/module/components/VideoFeed.js +2 -0
  36. package/lib/module/components/VideoFeed.js.map +1 -1
  37. package/lib/module/index.js +3 -1
  38. package/lib/module/index.js.map +1 -1
  39. package/lib/module/models/LiveStreamStatus.js +28 -0
  40. package/lib/module/models/LiveStreamStatus.js.map +1 -0
  41. package/lib/module/models/ScrollDirection.js +2 -0
  42. package/lib/module/models/ScrollDirection.js.map +1 -0
  43. package/lib/module/models/VideoType.js +22 -0
  44. package/lib/module/models/VideoType.js.map +1 -0
  45. package/lib/typescript/commonjs/src/components/StoryBlock.d.ts.map +1 -1
  46. package/lib/typescript/commonjs/src/components/VideoFeed.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/src/index.d.ts +5 -2
  48. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/src/models/FeedItemDetails.d.ts +12 -0
  50. package/lib/typescript/commonjs/src/models/FeedItemDetails.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/src/models/LiveStreamEventDetails.d.ts +23 -0
  52. package/lib/typescript/commonjs/src/models/LiveStreamEventDetails.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/src/models/LiveStreamStatus.d.ts +25 -0
  54. package/lib/typescript/commonjs/src/models/LiveStreamStatus.d.ts.map +1 -0
  55. package/lib/typescript/commonjs/src/models/ScrollDirection.d.ts +2 -0
  56. package/lib/typescript/commonjs/src/models/ScrollDirection.d.ts.map +1 -0
  57. package/lib/typescript/commonjs/src/models/StoryBlockConfiguration.d.ts +10 -0
  58. package/lib/typescript/commonjs/src/models/StoryBlockConfiguration.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/src/models/VideoPlaybackDetails.d.ts +12 -0
  60. package/lib/typescript/commonjs/src/models/VideoPlaybackDetails.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/src/models/VideoPlayerConfiguration.d.ts +5 -0
  62. package/lib/typescript/commonjs/src/models/VideoPlayerConfiguration.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/src/models/VideoType.d.ts +19 -0
  64. package/lib/typescript/commonjs/src/models/VideoType.d.ts.map +1 -0
  65. package/lib/typescript/module/src/components/StoryBlock.d.ts.map +1 -1
  66. package/lib/typescript/module/src/components/VideoFeed.d.ts.map +1 -1
  67. package/lib/typescript/module/src/index.d.ts +5 -2
  68. package/lib/typescript/module/src/index.d.ts.map +1 -1
  69. package/lib/typescript/module/src/models/FeedItemDetails.d.ts +12 -0
  70. package/lib/typescript/module/src/models/FeedItemDetails.d.ts.map +1 -1
  71. package/lib/typescript/module/src/models/LiveStreamEventDetails.d.ts +23 -0
  72. package/lib/typescript/module/src/models/LiveStreamEventDetails.d.ts.map +1 -1
  73. package/lib/typescript/module/src/models/LiveStreamStatus.d.ts +25 -0
  74. package/lib/typescript/module/src/models/LiveStreamStatus.d.ts.map +1 -0
  75. package/lib/typescript/module/src/models/ScrollDirection.d.ts +2 -0
  76. package/lib/typescript/module/src/models/ScrollDirection.d.ts.map +1 -0
  77. package/lib/typescript/module/src/models/StoryBlockConfiguration.d.ts +10 -0
  78. package/lib/typescript/module/src/models/StoryBlockConfiguration.d.ts.map +1 -1
  79. package/lib/typescript/module/src/models/VideoPlaybackDetails.d.ts +12 -0
  80. package/lib/typescript/module/src/models/VideoPlaybackDetails.d.ts.map +1 -1
  81. package/lib/typescript/module/src/models/VideoPlayerConfiguration.d.ts +5 -0
  82. package/lib/typescript/module/src/models/VideoPlayerConfiguration.d.ts.map +1 -1
  83. package/lib/typescript/module/src/models/VideoType.d.ts +19 -0
  84. package/lib/typescript/module/src/models/VideoType.d.ts.map +1 -0
  85. package/package.json +1 -1
  86. package/react_native_firework_sdk.podspec +1 -1
  87. package/src/components/StoryBlock.tsx +5 -1
  88. package/src/components/VideoFeed.tsx +2 -0
  89. package/src/index.tsx +6 -0
  90. package/src/models/FeedItemDetails.ts +12 -0
  91. package/src/models/LiveStreamEventDetails.ts +24 -1
  92. package/src/models/LiveStreamStatus.ts +25 -0
  93. package/src/models/ScrollDirection.ts +1 -0
  94. package/src/models/StoryBlockConfiguration.ts +12 -0
  95. package/src/models/VideoPlaybackDetails.ts +13 -0
  96. package/src/models/VideoPlayerConfiguration.ts +6 -0
  97. package/src/models/VideoType.ts +19 -0
@@ -6,4 +6,4 @@ FireworkSDK_targetSdkVersion=33
6
6
  FireworkSDK_compileSdkVersion=33
7
7
  FireworkSDK_kotlinVersion=1.8.22
8
8
  FireworkSDK_fwPlayerLaunchMode=singleTask
9
- FireworkSDK_fwNativeVersion=6.24.4
9
+ FireworkSDK_fwNativeVersion=6.26.0
@@ -2,10 +2,15 @@ package com.fireworksdk.bridge.components.storyblock
2
2
 
3
3
  import android.content.Context
4
4
  import android.util.AttributeSet
5
+ import android.view.View
5
6
  import android.widget.FrameLayout
7
+ import androidx.fragment.app.Fragment
6
8
  import androidx.fragment.app.FragmentActivity
9
+ import androidx.fragment.app.FragmentManager
10
+ import androidx.lifecycle.Lifecycle
11
+ import androidx.lifecycle.LifecycleEventObserver
7
12
  import com.fireworksdk.bridge.models.FWVideoFeedPropsModel
8
- import com.fireworksdk.bridge.utils.FWLogUtils
13
+ import com.fireworksdk.bridge.utils.FWFragmentUtil
9
14
 
10
15
  /**
11
16
  * StoryBlockContainerView provides safe Fragment management with automatic lifecycle handling.
@@ -21,66 +26,364 @@ class StoryBlockContainerView(
21
26
 
22
27
  private var isFragmentAttached = false
23
28
  private var pendingFragment: StoryBlockFragment? = null
29
+
30
+ private var retryCount = 0
31
+ private val maxRetryCount = 3
32
+
33
+ private var totalRetryCount = 0
34
+ private val maxTotalRetryCount = 6
35
+
36
+ private var isCallbackRegistered = false
37
+ private val pendingRunnables = mutableSetOf<Runnable>()
38
+ private var lifecycleObserver: LifecycleEventObserver? = null
39
+ private var isLifecycleObserverRegistered = false
24
40
 
25
41
  constructor(context: Context) : this(context, null)
26
42
 
43
+ /**
44
+ * Post a delayed runnable and track it for cleanup
45
+ */
46
+ private fun postDelayedTracked(delayMillis: Long, action: () -> Unit) {
47
+ val runnable = object : Runnable {
48
+ override fun run() {
49
+ pendingRunnables.remove(this)
50
+ action()
51
+ }
52
+ }
53
+ pendingRunnables.add(runnable)
54
+ postDelayed(runnable, delayMillis)
55
+ }
56
+
57
+ /**
58
+ * Post a runnable and track it for cleanup
59
+ */
60
+ private fun postTracked(action: () -> Unit) {
61
+ val runnable = object : Runnable {
62
+ override fun run() {
63
+ pendingRunnables.remove(this)
64
+ action()
65
+ }
66
+ }
67
+ pendingRunnables.add(runnable)
68
+ post(runnable)
69
+ }
70
+
71
+ /**
72
+ * Remove all pending callbacks to prevent memory leaks
73
+ */
74
+ private fun removeAllCallbacks() {
75
+ if (pendingRunnables.isNotEmpty()) {
76
+ val runnablesToRemove = pendingRunnables.toList()
77
+ runnablesToRemove.forEach { runnable ->
78
+ removeCallbacks(runnable)
79
+ }
80
+ pendingRunnables.clear()
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Check if the given fragment is our StoryBlockFragment
86
+ * Uses multiple criteria: reference equality, tag matching, and type check
87
+ */
88
+ private fun isOurFragment(f: Fragment): Boolean {
89
+ if (f == storyBlockFragment) return true
90
+ if (f.tag == id.toString() && f is StoryBlockFragment) return true
91
+ return false
92
+ }
93
+
94
+ /**
95
+ * Check if fragment can be attached to this container
96
+ * Returns true when fragment is not attached and this view is attached to window
97
+ */
98
+ private fun canAttachFragment(fragment: StoryBlockFragment): Boolean {
99
+ return !isFragmentAttached && !fragment.isAdded && isAttachedToWindow
100
+ }
101
+
102
+ /**
103
+ * Check if fragment is already attached
104
+ * Opposite of canAttachFragment()
105
+ */
106
+ private fun isFragmentAlreadyAttached(fragment: StoryBlockFragment): Boolean {
107
+ return isFragmentAttached || fragment.isAdded
108
+ }
109
+
110
+ private fun isActivityFinished(activity: FragmentActivity): Boolean{
111
+ return activity.isFinishing || activity.isDestroyed
112
+ }
113
+
114
+ /**
115
+ * Fragment lifecycle callbacks to monitor fragment attachment state
116
+ */
117
+ private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
118
+ override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
119
+ if (isOurFragment(f) && f is StoryBlockFragment) {
120
+ isFragmentAttached = true
121
+ retryCount = 0
122
+ totalRetryCount = 0
123
+ storyBlockFragment = f
124
+ }
125
+ }
126
+
127
+ override fun onFragmentDetached(fm: FragmentManager, f: Fragment) {
128
+ if (isOurFragment(f)) {
129
+ isFragmentAttached = false
130
+ }
131
+ }
132
+ }
133
+
27
134
  override fun onAttachedToWindow() {
28
135
  super.onAttachedToWindow()
29
- // Try to attach pending fragment when view is attached
136
+ // Try to attach pending fragment when view is attached to window
30
137
  pendingFragment?.let { fragment ->
31
- attachFragmentSafely(fragment)
138
+ val savedPendingFragment = fragment
32
139
  pendingFragment = null
140
+
141
+ try {
142
+ attachFragmentSafely(savedPendingFragment)
143
+ } catch (_: Exception) {
144
+ if (pendingFragment == null) {
145
+ if (!isFragmentAttached) {
146
+ pendingFragment = savedPendingFragment
147
+ }
148
+ }
149
+ }
33
150
  }
34
151
  }
35
152
 
36
153
  override fun onDetachedFromWindow() {
37
154
  super.onDetachedFromWindow()
38
- // Fragment will be automatically detached by FragmentManager
155
+
156
+ // Clean up all pending callbacks
157
+ removeAllCallbacks()
158
+
159
+ // Unregister lifecycle observer
160
+ (context as? FragmentActivity)?.let { activity ->
161
+ unregisterLifecycleObserver(activity)
162
+ }
163
+
164
+ // Unregister fragment lifecycle callbacks
165
+ if (isCallbackRegistered) {
166
+ (context as? FragmentActivity)?.supportFragmentManager?.let { fm ->
167
+ fm.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks)
168
+ isCallbackRegistered = false
169
+ }
170
+ }
171
+
172
+ // Clear all references to prevent memory leaks
173
+ // pendingFragment = null
174
+ // storyBlockFragment = null
39
175
  isFragmentAttached = false
176
+ retryCount = 0
177
+ totalRetryCount = 0
178
+ }
179
+
180
+ /**
181
+ * Verify if this view's ID can be found in the Activity's view hierarchy
182
+ */
183
+ private fun isViewInActivityHierarchy(activity: FragmentActivity): Boolean {
184
+ return try {
185
+ val contentView = activity.findViewById<View>(android.R.id.content)
186
+ val foundView = contentView?.findViewById<View>(id)
187
+ foundView === this
188
+ } catch (_: Exception) {
189
+ false
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Check if it's safe to execute Fragment transactions
195
+ * Only returns true when Activity is RESUMED and state is not saved
196
+ */
197
+ private fun isSafeToCommit(activity: FragmentActivity): Boolean {
198
+ val currentState = activity.lifecycle.currentState
199
+ val isStateSaved = activity.supportFragmentManager.isStateSaved
200
+ return currentState.isAtLeast(Lifecycle.State.RESUMED) && !isStateSaved
40
201
  }
41
202
 
42
203
  /**
43
- * Safely attach fragment to this container
204
+ * Safely attach fragment to this container with retry mechanism
205
+ * - Checks total retry limit (defensive programming)
206
+ * - Validates Activity and View state
207
+ * - Uses exponential backoff for retries
208
+ * - Defers to lifecycle observer if Activity is not RESUMED
44
209
  */
45
210
  fun attachFragmentSafely(fragment: StoryBlockFragment) {
46
- if (isFragmentAttached || fragment.isAdded) {
211
+ // Defensive programming: absolute limit on total retry attempts
212
+ if (totalRetryCount >= maxTotalRetryCount) {
47
213
  return
48
214
  }
49
-
50
- val activity = context as? FragmentActivity ?: run {
51
- FWLogUtils.e { "Context is not FragmentActivity, cannot attach fragment" }
215
+
216
+ if (isFragmentAlreadyAttached(fragment)) {
52
217
  return
53
218
  }
54
219
 
55
- if (activity.isFinishing || activity.isDestroyed) {
56
- FWLogUtils.w { "Activity is finishing or destroyed, cannot attach fragment" }
220
+ // Increment total retry counter
221
+ totalRetryCount++
222
+
223
+ val activity = context as? FragmentActivity ?: return
224
+
225
+ if (isActivityFinished(activity)) {
57
226
  return
58
227
  }
59
228
 
60
- try {
61
- val fragmentManager = activity.supportFragmentManager
62
- fragmentManager.beginTransaction()
63
- .replace(id, fragment, id.toString())
64
- .commitAllowingStateLoss()
229
+ // Ensure View has a valid ID
230
+ if (id == NO_ID) {
231
+ pendingFragment = fragment
232
+ return
233
+ }
234
+
235
+ // Ensure View is attached to window and has a parent
236
+ if (!isAttachedToWindow || parent == null) {
237
+ pendingFragment = fragment
238
+ return
239
+ }
240
+
241
+ // Check if view is in activity hierarchy before attempting fragment transaction
242
+ if (!isViewInActivityHierarchy(activity)) {
243
+ if (retryCount >= maxRetryCount) {
244
+ return
245
+ }
246
+
247
+ retryCount++
248
+ val delayMs = 50L * retryCount // Exponential backoff: 50ms, 100ms, 150ms
249
+
250
+ postDelayedTracked(delayMs) {
251
+ if (totalRetryCount >= maxTotalRetryCount) {
252
+ return@postDelayedTracked
253
+ }
254
+
255
+ if (canAttachFragment(fragment)) {
256
+ attachFragmentSafely(fragment)
257
+ }
258
+ }
259
+ return
260
+ }
261
+
262
+ val fm = activity.supportFragmentManager
263
+
264
+ // Register lifecycle callbacks to monitor fragment attachment (only once)
265
+ if (!isCallbackRegistered) {
266
+ fm.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false)
267
+ isCallbackRegistered = true
268
+ }
269
+
270
+ // Delay to ensure the view hierarchy is fully laid out
271
+ postTracked {
272
+ if (!canAttachFragment(fragment)) {
273
+ return@postTracked
274
+ }
65
275
 
66
- // Delay state update to ensure transaction is committed
67
- post {
68
- isFragmentAttached = fragment.isAdded
69
- if (isFragmentAttached) {
70
- FWLogUtils.d { "Fragment attached successfully to container $id" }
71
- } else {
72
- FWLogUtils.w { "Fragment attachment may have failed, fragment.isAdded = false" }
276
+ // Double-check view is still in activity hierarchy (race condition prevention)
277
+ val currentActivity = context as? FragmentActivity ?: return@postTracked
278
+
279
+ if (!isViewInActivityHierarchy(currentActivity)) {
280
+ return@postTracked
281
+ }
282
+
283
+ // Double-check activity state (race condition prevention)
284
+ if (isActivityFinished(currentActivity)) {
285
+ return@postTracked
286
+ }
287
+
288
+ try {
289
+ executeFragmentTransaction(currentActivity, fragment, fm)
290
+ } catch (_: IllegalArgumentException) {
291
+ handleFragmentTransactionException(fragment, fm)
292
+ } catch (_: IllegalStateException) {
293
+ handleIllegalStateException(fragment)
294
+ } catch (_: Exception) {
295
+ handleUnexpectedException(fragment, fm)
296
+ }
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Execute Fragment transaction with lifecycle-aware commit strategy
302
+ * Uses commitNow() only when Activity is RESUMED to avoid commitAllowingStateLoss()
303
+ */
304
+ private fun executeFragmentTransaction(
305
+ activity: FragmentActivity,
306
+ fragment: StoryBlockFragment,
307
+ fm: FragmentManager
308
+ ) {
309
+ if (isSafeToCommit(activity)) {
310
+ fm.beginTransaction()
311
+ .replace(id, fragment, id.toString())
312
+ .commitNow() // Synchronous execution, exceptions can be caught
313
+ retryCount = 0
314
+ } else {
315
+ // Defer transaction until Activity becomes RESUMED
316
+ throw IllegalStateException("Cannot commit: lifecycle not RESUMED or state saved")
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Handle IllegalArgumentException during fragment transaction
322
+ * Typically caused by "No view found for id" errors
323
+ */
324
+ private fun handleFragmentTransactionException(
325
+ fragment: StoryBlockFragment,
326
+ fm: FragmentManager
327
+ ) {
328
+ if (isCallbackRegistered) {
329
+ fm.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks)
330
+ isCallbackRegistered = false
331
+ }
332
+
333
+ if (retryCount < maxRetryCount) {
334
+ retryCount++
335
+ val delayMs = 100L * retryCount
336
+ postDelayedTracked(delayMs) {
337
+ if (totalRetryCount >= maxTotalRetryCount) {
338
+ return@postDelayedTracked
339
+ }
340
+
341
+ if (canAttachFragment(fragment)) {
342
+ attachFragmentSafely(fragment)
73
343
  }
74
344
  }
75
- } catch (e: Exception) {
76
- FWLogUtils.e { "Failed to attach fragment: ${e.message}" }
77
- // Store fragment for later attachment
78
- pendingFragment = fragment
345
+ } else {
346
+ retryCount = 0
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Handle IllegalStateException during fragment transaction
352
+ * Saves fragment as pending and registers lifecycle observer to retry when Activity becomes RESUMED
353
+ */
354
+ private fun handleIllegalStateException(
355
+ fragment: StoryBlockFragment,
356
+ ) {
357
+ val activity = (context as? FragmentActivity) ?: return
358
+
359
+ pendingFragment = fragment
360
+ retryCount = 0
361
+
362
+ if (!isLifecycleObserverRegistered) {
363
+ registerLifecycleObserver(activity)
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Handle unexpected exceptions during fragment transaction
369
+ * Stores fragment for later attachment
370
+ */
371
+ private fun handleUnexpectedException(
372
+ fragment: StoryBlockFragment,
373
+ fm: FragmentManager
374
+ ) {
375
+ if (isCallbackRegistered) {
376
+ fm.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks)
377
+ isCallbackRegistered = false
79
378
  }
379
+
380
+ pendingFragment = fragment
80
381
  }
81
382
 
82
383
  /**
83
384
  * Set fragment to be attached when container is ready
385
+ * If view is already attached to window, attempts immediate attachment
386
+ * Otherwise, defers attachment until onAttachedToWindow()
84
387
  */
85
388
  fun setFragment(fragment: StoryBlockFragment) {
86
389
  storyBlockFragment = fragment
@@ -90,14 +393,117 @@ class StoryBlockContainerView(
90
393
  pendingFragment = fragment
91
394
  }
92
395
  }
93
-
396
+
397
+ /**
398
+ * Create lifecycle observer to monitor Activity state changes
399
+ * Automatically retries fragment attachment when Activity becomes RESUMED
400
+ */
401
+ private fun createLifecycleObserver(activity: FragmentActivity): LifecycleEventObserver {
402
+ return LifecycleEventObserver { _, event ->
403
+ when (event) {
404
+ Lifecycle.Event.ON_RESUME -> {
405
+ pendingFragment?.let { fragment ->
406
+ if (canAttachFragment(fragment)) {
407
+ // Reset retry counters when Activity reaches RESUMED state
408
+ // This is the ideal time to attach fragments, so we give it a fresh chance
409
+ // even if previous attempts failed during non-ideal lifecycle states
410
+ if (totalRetryCount >= maxTotalRetryCount) {
411
+ totalRetryCount = 0
412
+ retryCount = 0
413
+ }
414
+
415
+ try {
416
+ attachFragmentSafely(fragment)
417
+ } catch (_: Exception) {
418
+ // Ignore
419
+ }
420
+ } else {
421
+ if (isFragmentAlreadyAttached(fragment)) {
422
+ pendingFragment = null
423
+ }
424
+ }
425
+ }
426
+ }
427
+
428
+ Lifecycle.Event.ON_DESTROY -> {
429
+ unregisterLifecycleObserver(activity)
430
+ pendingFragment = null
431
+ }
432
+
433
+ else -> {}
434
+ }
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Register lifecycle observer to automatically retry when Activity becomes RESUMED
440
+ */
441
+ private fun registerLifecycleObserver(activity: FragmentActivity) {
442
+ if (isLifecycleObserverRegistered) {
443
+ return
444
+ }
445
+
446
+ val observer = createLifecycleObserver(activity)
447
+ activity.lifecycle.addObserver(observer)
448
+
449
+ lifecycleObserver = observer
450
+ isLifecycleObserverRegistered = true
451
+ }
452
+
94
453
  /**
95
- * Clean up resources when the view is destroyed
454
+ * Unregister lifecycle observer to prevent memory leaks
96
455
  */
456
+ private fun unregisterLifecycleObserver(activity: FragmentActivity) {
457
+ if (!isLifecycleObserverRegistered) {
458
+ return
459
+ }
460
+
461
+ lifecycleObserver?.let { observer ->
462
+ try {
463
+ if (activity.lifecycle.currentState != Lifecycle.State.DESTROYED) {
464
+ activity.lifecycle.removeObserver(observer)
465
+ }
466
+ } catch (_: Exception) {
467
+ // Ignore
468
+ }
469
+ }
470
+
471
+ lifecycleObserver = null
472
+ isLifecycleObserverRegistered = false
473
+ }
474
+
97
475
  override fun destroy() {
98
- // Fragment will be automatically cleaned up by FragmentManager
476
+ removeAllCallbacks()
477
+
478
+ // Capture fragment reference before clearing to prevent race condition
479
+ // This ensures the fragment can be properly cleaned up even if dispose() is called concurrently
480
+ val fragmentToCleanup = storyBlockFragment
481
+
482
+ (context as? FragmentActivity)?.let { activity ->
483
+ unregisterLifecycleObserver(activity)
484
+
485
+ // Clean up fragment before clearing the reference
486
+ if (fragmentToCleanup != null) {
487
+ try {
488
+ fragmentToCleanup.removeIvsPlayerView()
489
+ FWFragmentUtil.removeFragment(activity, fragmentToCleanup)
490
+ } catch (e: Exception) {
491
+ // Log but don't crash
492
+ }
493
+ }
494
+ }
495
+
496
+ if (isCallbackRegistered) {
497
+ (context as? FragmentActivity)?.supportFragmentManager?.let { fm ->
498
+ fm.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks)
499
+ isCallbackRegistered = false
500
+ }
501
+ }
502
+
99
503
  isFragmentAttached = false
100
504
  pendingFragment = null
101
505
  storyBlockFragment = null
506
+ retryCount = 0
507
+ totalRetryCount = 0
102
508
  }
103
509
  }
@@ -6,6 +6,7 @@ object FWPlayerButtonConfigurationDeserializer {
6
6
 
7
7
  private const val VIDEO_DETAIL_BUTTON_KEY = "videoDetailButton"
8
8
  private const val CLOSE_BUTTON_KEY = "closeButton"
9
+ private const val SHOULD_SHOW_CLOSE_BUTTON_WHEN_PIP_ENABLED_KEY = "shouldShowCloseButtonWhenPiPEnabled"
9
10
  private const val PIP_BUTTON_KEY = "pipButton"
10
11
  private const val MUTE_BUTTON_KEY = "muteButton"
11
12
  private const val UNMUTE_BUTTON_KEY = "unmuteButton"
@@ -17,6 +18,7 @@ object FWPlayerButtonConfigurationDeserializer {
17
18
 
18
19
  val videoDetailButtonJsonObject = responseJson.optJSONObject(VIDEO_DETAIL_BUTTON_KEY)
19
20
  val closeButtonJsonObject = responseJson.optJSONObject(CLOSE_BUTTON_KEY)
21
+ val shouldShowCloseButtonWhenPiPEnabled = responseJson.optBoolean(SHOULD_SHOW_CLOSE_BUTTON_WHEN_PIP_ENABLED_KEY, false)
20
22
  val pipButtonJsonObject = responseJson.optJSONObject(PIP_BUTTON_KEY)
21
23
  val muteButtonJsonObject = responseJson.optJSONObject(MUTE_BUTTON_KEY)
22
24
  val unmuteButtonJsonObject = responseJson.optJSONObject(UNMUTE_BUTTON_KEY)
@@ -26,6 +28,7 @@ object FWPlayerButtonConfigurationDeserializer {
26
28
  return FWPlayerButtonConfigurationModel(
27
29
  videoDetailButton = FWButtonInfoDeserializer.deserialize(videoDetailButtonJsonObject),
28
30
  closeButton = FWButtonInfoDeserializer.deserialize(closeButtonJsonObject),
31
+ shouldShowCloseButtonWhenPiPEnabled = shouldShowCloseButtonWhenPiPEnabled,
29
32
  pipButton = FWButtonInfoDeserializer.deserialize(pipButtonJsonObject),
30
33
  muteButton = FWButtonInfoDeserializer.deserialize(muteButtonJsonObject),
31
34
  unmuteButton = FWButtonInfoDeserializer.deserialize(unmuteButtonJsonObject),
@@ -3,6 +3,7 @@ package com.fireworksdk.bridge.models
3
3
  data class FWPlayerButtonConfigurationModel(
4
4
  val videoDetailButton: FWButtonInfoModel? = null,
5
5
  val closeButton: FWButtonInfoModel? = null,
6
+ val shouldShowCloseButtonWhenPiPEnabled: Boolean? = null,
6
7
  val pipButton: FWButtonInfoModel? = null,
7
8
  val muteButton: FWButtonInfoModel? = null,
8
9
  val unmuteButton: FWButtonInfoModel? = null,
@@ -6,6 +6,7 @@ object FWPlayerButtonConfigurationSerializer {
6
6
 
7
7
  private const val VIDEO_DETAIL_BUTTON_KEY = "videoDetailButton"
8
8
  private const val CLOSE_BUTTON_KEY = "closeButton"
9
+ private const val SHOULD_SHOW_CLOSE_BUTTON_WHEN_PIP_ENABLED_KEY = "shouldShowCloseButtonWhenPiPEnabled"
9
10
  private const val PIP_BUTTON_KEY = "pipButton"
10
11
  private const val MUTE_BUTTON_KEY = "muteButton"
11
12
  private const val UNMUTE_BUTTON_KEY = "unmuteButton"
@@ -17,6 +18,7 @@ object FWPlayerButtonConfigurationSerializer {
17
18
  val jsonObject = JSONObject()
18
19
  jsonObject.put(VIDEO_DETAIL_BUTTON_KEY, FWButtonInfoSerializer.serialize(model.videoDetailButton))
19
20
  jsonObject.put(CLOSE_BUTTON_KEY, FWButtonInfoSerializer.serialize(model.closeButton))
21
+ jsonObject.put(SHOULD_SHOW_CLOSE_BUTTON_WHEN_PIP_ENABLED_KEY, model.shouldShowCloseButtonWhenPiPEnabled)
20
22
  jsonObject.put(PIP_BUTTON_KEY, FWButtonInfoSerializer.serialize(model.pipButton))
21
23
  jsonObject.put(MUTE_BUTTON_KEY, FWButtonInfoSerializer.serialize(model.muteButton))
22
24
  jsonObject.put(UNMUTE_BUTTON_KEY, FWButtonInfoSerializer.serialize(model.unmuteButton))
@@ -4,9 +4,12 @@ import com.fireworksdk.bridge.models.enums.FWCtaDelayType
4
4
  import com.fireworksdk.bridge.models.enums.FWPlayerStyle
5
5
  import com.fireworksdk.bridge.models.enums.FWVideoCompleteAction
6
6
  import com.fireworksdk.bridge.models.enums.FWVideoPlayerCTAWidth
7
+ import com.fireworksdk.bridge.models.enums.FWVideoPlayerScrollDirection
7
8
 
8
9
  data class FWVideoPlayerConfigModel(
9
10
  val playerStyle: FWPlayerStyle? = null,
11
+ val scrollDirection: FWVideoPlayerScrollDirection? = null,
12
+ val enableScrollForVertical: Boolean? = null,
10
13
  val videoCompleteAction: FWVideoCompleteAction? = null,
11
14
  val showShareButton: Boolean? = null,
12
15
  val ctaButtonStyle: FWCtaButtonStyleModel? = null,
@@ -4,11 +4,14 @@ import com.fireworksdk.bridge.models.enums.FWCtaDelayType
4
4
  import com.fireworksdk.bridge.models.enums.FWPlayerStyle
5
5
  import com.fireworksdk.bridge.models.enums.FWVideoCompleteAction
6
6
  import com.fireworksdk.bridge.models.enums.FWVideoPlayerCTAWidth
7
+ import com.fireworksdk.bridge.models.enums.FWVideoPlayerScrollDirection
7
8
  import org.json.JSONObject
8
9
 
9
10
  object FWVideoPlayerConfigModelDeserializer {
10
11
 
11
12
  private const val PLAYER_STYLE_KEY = "playerStyle"
13
+ private const val SCROLL_DIRECTION_KEY = "scrollDirection"
14
+ private const val ENABLE_SCROLL_FOR_VERTICAL_KEY = "enableScrollForVertical"
12
15
  private const val VIDEO_COMPLETE_ACTION_KEY = "videoCompleteAction"
13
16
  private const val SHOW_SHARE_BUTTON_KEY = "showShareButton"
14
17
  private const val CTA_BUTTON_STYLE_KEY = "ctaButtonStyle"
@@ -50,6 +53,8 @@ object FWVideoPlayerConfigModelDeserializer {
50
53
  responseJson ?: return null
51
54
 
52
55
  val playerStyle = if (responseJson.has(PLAYER_STYLE_KEY)) responseJson.optString(PLAYER_STYLE_KEY) else null
56
+ val scrollDirection = if (responseJson.has(SCROLL_DIRECTION_KEY)) responseJson.optString(SCROLL_DIRECTION_KEY) else null
57
+ val enableScrollForVertical = if (responseJson.has(ENABLE_SCROLL_FOR_VERTICAL_KEY)) responseJson.optBoolean(ENABLE_SCROLL_FOR_VERTICAL_KEY) else null
53
58
  val videoCompleteAction = if (responseJson.has(VIDEO_COMPLETE_ACTION_KEY)) responseJson.optString(VIDEO_COMPLETE_ACTION_KEY) else null
54
59
  val showShareButton = if (responseJson.has(SHOW_SHARE_BUTTON_KEY)) responseJson.optBoolean(SHOW_SHARE_BUTTON_KEY) else null
55
60
  val ctaButtonStyle = deserializeCtaButtonStyle(responseJson.optJSONObject(CTA_BUTTON_STYLE_KEY))
@@ -84,6 +89,8 @@ object FWVideoPlayerConfigModelDeserializer {
84
89
 
85
90
  return FWVideoPlayerConfigModel(
86
91
  playerStyle = if (!playerStyle.isNullOrBlank()) FWPlayerStyle.deserialize(playerStyle) else null,
92
+ scrollDirection = if (!scrollDirection.isNullOrBlank()) FWVideoPlayerScrollDirection.deserialize(scrollDirection) else null,
93
+ enableScrollForVertical = enableScrollForVertical,
87
94
  videoCompleteAction = if (!videoCompleteAction.isNullOrBlank()) FWVideoCompleteAction.deserialize(videoCompleteAction) else null,
88
95
  showShareButton = showShareButton,
89
96
  ctaButtonStyle = ctaButtonStyle,
@@ -4,11 +4,14 @@ import com.fireworksdk.bridge.models.enums.FWCtaDelayType
4
4
  import com.fireworksdk.bridge.models.enums.FWPlayerStyle
5
5
  import com.fireworksdk.bridge.models.enums.FWVideoCompleteAction
6
6
  import com.fireworksdk.bridge.models.enums.FWVideoPlayerCTAWidth
7
+ import com.fireworksdk.bridge.models.enums.FWVideoPlayerScrollDirection
7
8
  import org.json.JSONObject
8
9
 
9
10
  object FWVideoPlayerConfigModelSerializer {
10
11
 
11
12
  private const val PLAYER_STYLE_KEY = "playerStyle"
13
+ private const val SCROLL_DIRECTION_KEY = "scrollDirection"
14
+ private const val ENABLE_SCROLL_FOR_VERTICAL_KEY = "enableScrollForVertical"
12
15
  private const val VIDEO_COMPLETE_ACTION_KEY = "videoCompleteAction"
13
16
  private const val SHOW_SHARE_BUTTON_KEY = "showShareButton"
14
17
  private const val CTA_BUTTON_STYLE_KEY = "ctaButtonStyle"
@@ -55,6 +58,8 @@ object FWVideoPlayerConfigModelSerializer {
55
58
  model ?: return null
56
59
  val jsonObject = JSONObject()
57
60
  jsonObject.put(PLAYER_STYLE_KEY, FWPlayerStyle.serialize(model.playerStyle))
61
+ jsonObject.put(SCROLL_DIRECTION_KEY, FWVideoPlayerScrollDirection.serialize(model.scrollDirection))
62
+ jsonObject.put(ENABLE_SCROLL_FOR_VERTICAL_KEY, model.enableScrollForVertical)
58
63
  jsonObject.put(VIDEO_COMPLETE_ACTION_KEY, FWVideoCompleteAction.serialize(model.videoCompleteAction))
59
64
  jsonObject.put(SHOW_SHARE_BUTTON_KEY, model.showShareButton)
60
65
  jsonObject.put(CTA_BUTTON_STYLE_KEY, serializeCtaButtonStyle(model.ctaButtonStyle))
@@ -0,0 +1,17 @@
1
+ package com.fireworksdk.bridge.models.enums
2
+
3
+ enum class FWVideoPlayerScrollDirection(val rawValue: String) {
4
+ Vertical("vertical"),
5
+ Horizontal("horizontal");
6
+
7
+ companion object {
8
+ fun deserialize(rawValue: String): FWVideoPlayerScrollDirection? {
9
+ return values().firstOrNull { it.rawValue == rawValue }
10
+ }
11
+
12
+ fun serialize(value: FWVideoPlayerScrollDirection?): String? {
13
+ return value?.rawValue
14
+ }
15
+ }
16
+ }
17
+
@@ -0,0 +1,6 @@
1
+ package com.fireworksdk.bridge.models.enums
2
+
3
+ enum class FWVideoPlayerVersion(val rawValue: String) {
4
+ V1("v1"),
5
+ V2("v2"),
6
+ }