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.
- package/android/gradle.properties +1 -1
- package/android/src/main/java/com/fireworksdk/bridge/components/storyblock/StoryBlockContainerView.kt +436 -30
- package/android/src/main/java/com/fireworksdk/bridge/models/FWPlayerButtonConfigurationDeserializer.kt +3 -0
- package/android/src/main/java/com/fireworksdk/bridge/models/FWPlayerButtonConfigurationModel.kt +1 -0
- package/android/src/main/java/com/fireworksdk/bridge/models/FWPlayerButtonConfigurationSerializer.kt +2 -0
- package/android/src/main/java/com/fireworksdk/bridge/models/FWVideoPlayerConfigModel.kt +3 -0
- package/android/src/main/java/com/fireworksdk/bridge/models/FWVideoPlayerConfigModelDeserializer.kt +7 -0
- package/android/src/main/java/com/fireworksdk/bridge/models/FWVideoPlayerConfigModelSerializer.kt +5 -0
- package/android/src/main/java/com/fireworksdk/bridge/models/enums/FWVideoPlayerScrollDirection.kt +17 -0
- package/android/src/main/java/com/fireworksdk/bridge/models/enums/FWVideoPlayerVersion.kt +6 -0
- package/android/src/main/java/com/fireworksdk/bridge/reactnative/manager/FWVideoFeedManager.kt +2 -2
- package/android/src/main/java/com/fireworksdk/bridge/reactnative/module/FireworkSDKModule.kt +3 -2
- package/android/src/main/java/com/fireworksdk/bridge/reactnative/utils/FWEventUtils.kt +22 -0
- package/android/src/main/java/com/fireworksdk/bridge/utils/FWConfigUtil.kt +23 -2
- package/android/src/main/java/com/fireworksdk/bridge/utils/FWLanguageUtil.kt +11 -1
- package/ios/Components/StoryBlock.swift +15 -0
- package/ios/Components/StoryBlockConfiguration.swift +2 -0
- package/ios/Components/VideoFeed.swift +9 -0
- package/ios/Components/VideoPlayerConfiguration.swift +5 -0
- package/ios/Models/NativeToRN/FireworkSDK+Json.swift +33 -3
- package/lib/commonjs/components/StoryBlock.js +4 -0
- package/lib/commonjs/components/StoryBlock.js.map +1 -1
- package/lib/commonjs/components/VideoFeed.js +2 -0
- package/lib/commonjs/components/VideoFeed.js.map +1 -1
- package/lib/commonjs/index.js +14 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/models/LiveStreamStatus.js +32 -0
- package/lib/commonjs/models/LiveStreamStatus.js.map +1 -0
- package/lib/commonjs/models/ScrollDirection.js +2 -0
- package/lib/commonjs/models/ScrollDirection.js.map +1 -0
- package/lib/commonjs/models/VideoType.js +26 -0
- package/lib/commonjs/models/VideoType.js.map +1 -0
- package/lib/module/components/StoryBlock.js +4 -0
- package/lib/module/components/StoryBlock.js.map +1 -1
- package/lib/module/components/VideoFeed.js +2 -0
- package/lib/module/components/VideoFeed.js.map +1 -1
- package/lib/module/index.js +3 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/models/LiveStreamStatus.js +28 -0
- package/lib/module/models/LiveStreamStatus.js.map +1 -0
- package/lib/module/models/ScrollDirection.js +2 -0
- package/lib/module/models/ScrollDirection.js.map +1 -0
- package/lib/module/models/VideoType.js +22 -0
- package/lib/module/models/VideoType.js.map +1 -0
- package/lib/typescript/commonjs/src/components/StoryBlock.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/components/VideoFeed.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +5 -2
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/models/FeedItemDetails.d.ts +12 -0
- package/lib/typescript/commonjs/src/models/FeedItemDetails.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/models/LiveStreamEventDetails.d.ts +23 -0
- package/lib/typescript/commonjs/src/models/LiveStreamEventDetails.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/models/LiveStreamStatus.d.ts +25 -0
- package/lib/typescript/commonjs/src/models/LiveStreamStatus.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/models/ScrollDirection.d.ts +2 -0
- package/lib/typescript/commonjs/src/models/ScrollDirection.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/models/StoryBlockConfiguration.d.ts +10 -0
- package/lib/typescript/commonjs/src/models/StoryBlockConfiguration.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/models/VideoPlaybackDetails.d.ts +12 -0
- package/lib/typescript/commonjs/src/models/VideoPlaybackDetails.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/models/VideoPlayerConfiguration.d.ts +5 -0
- package/lib/typescript/commonjs/src/models/VideoPlayerConfiguration.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/models/VideoType.d.ts +19 -0
- package/lib/typescript/commonjs/src/models/VideoType.d.ts.map +1 -0
- package/lib/typescript/module/src/components/StoryBlock.d.ts.map +1 -1
- package/lib/typescript/module/src/components/VideoFeed.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +5 -2
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/models/FeedItemDetails.d.ts +12 -0
- package/lib/typescript/module/src/models/FeedItemDetails.d.ts.map +1 -1
- package/lib/typescript/module/src/models/LiveStreamEventDetails.d.ts +23 -0
- package/lib/typescript/module/src/models/LiveStreamEventDetails.d.ts.map +1 -1
- package/lib/typescript/module/src/models/LiveStreamStatus.d.ts +25 -0
- package/lib/typescript/module/src/models/LiveStreamStatus.d.ts.map +1 -0
- package/lib/typescript/module/src/models/ScrollDirection.d.ts +2 -0
- package/lib/typescript/module/src/models/ScrollDirection.d.ts.map +1 -0
- package/lib/typescript/module/src/models/StoryBlockConfiguration.d.ts +10 -0
- package/lib/typescript/module/src/models/StoryBlockConfiguration.d.ts.map +1 -1
- package/lib/typescript/module/src/models/VideoPlaybackDetails.d.ts +12 -0
- package/lib/typescript/module/src/models/VideoPlaybackDetails.d.ts.map +1 -1
- package/lib/typescript/module/src/models/VideoPlayerConfiguration.d.ts +5 -0
- package/lib/typescript/module/src/models/VideoPlayerConfiguration.d.ts.map +1 -1
- package/lib/typescript/module/src/models/VideoType.d.ts +19 -0
- package/lib/typescript/module/src/models/VideoType.d.ts.map +1 -0
- package/package.json +1 -1
- package/react_native_firework_sdk.podspec +1 -1
- package/src/components/StoryBlock.tsx +5 -1
- package/src/components/VideoFeed.tsx +2 -0
- package/src/index.tsx +6 -0
- package/src/models/FeedItemDetails.ts +12 -0
- package/src/models/LiveStreamEventDetails.ts +24 -1
- package/src/models/LiveStreamStatus.ts +25 -0
- package/src/models/ScrollDirection.ts +1 -0
- package/src/models/StoryBlockConfiguration.ts +12 -0
- package/src/models/VideoPlaybackDetails.ts +13 -0
- package/src/models/VideoPlayerConfiguration.ts +6 -0
- package/src/models/VideoType.ts +19 -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
+
// Defensive programming: absolute limit on total retry attempts
|
|
212
|
+
if (totalRetryCount >= maxTotalRetryCount) {
|
|
47
213
|
return
|
|
48
214
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
FWLogUtils.e { "Context is not FragmentActivity, cannot attach fragment" }
|
|
215
|
+
|
|
216
|
+
if (isFragmentAlreadyAttached(fragment)) {
|
|
52
217
|
return
|
|
53
218
|
}
|
|
54
219
|
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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),
|
package/android/src/main/java/com/fireworksdk/bridge/models/FWPlayerButtonConfigurationModel.kt
CHANGED
|
@@ -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,
|
package/android/src/main/java/com/fireworksdk/bridge/models/FWPlayerButtonConfigurationSerializer.kt
CHANGED
|
@@ -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,
|
package/android/src/main/java/com/fireworksdk/bridge/models/FWVideoPlayerConfigModelDeserializer.kt
CHANGED
|
@@ -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,
|
package/android/src/main/java/com/fireworksdk/bridge/models/FWVideoPlayerConfigModelSerializer.kt
CHANGED
|
@@ -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))
|
package/android/src/main/java/com/fireworksdk/bridge/models/enums/FWVideoPlayerScrollDirection.kt
ADDED
|
@@ -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
|
+
|