@@ -11,6 +11,7 @@ import org.junit.Test
1111import org.junit.runner.RunWith
1212import java.util.concurrent.CountDownLatch
1313import java.util.concurrent.TimeUnit
14+ import java.util.concurrent.atomic.AtomicReference
1415import java.util.concurrent.locks.ReentrantLock
1516
1617@RunWith(AndroidJUnit4 ::class )
@@ -255,4 +256,72 @@ class RiveArtboardRendererTest {
255256
256257 drawThread.join(timeout)
257258 }
259+
260+ /* *
261+ * Tests that the renderer can be safely deleted while resizeArtboard() is executing.
262+ * The fix adds a hasCppObject check at the start of resizeArtboard() to prevent
263+ * accessing disposed C++ objects when accessing width/height properties.
264+ */
265+ @Test
266+ fun deleteRendererDuringResizeArtboard () {
267+ val timeout = 1000L
268+ // Latch to signal we've entered resizeArtboard()
269+ val duringResizeLatch = CountDownLatch (1 )
270+ // Latch to block until we have deleted the renderer
271+ val afterDeleteLatch = CountDownLatch (1 )
272+
273+ val controller = RiveFileController ()
274+ controller.fit = Fit .LAYOUT // This sets requireArtboardResize to true
275+ controller.isActive = true
276+
277+ // Create a custom renderer that overrides resizeArtboard() to add blocking,
278+ // simulating the race condition where the renderer is deleted during resize
279+ val latchingRenderer = object : RiveArtboardRenderer (controller = controller) {
280+ override fun resizeArtboard () {
281+ // Signal we've entered resizeArtboard()
282+ duringResizeLatch.countDown()
283+
284+ // Block until the renderer is deleted - this simulates the race where
285+ // the renderer could be deleted while resizeArtboard() is executing
286+ afterDeleteLatch.await(timeout, TimeUnit .MILLISECONDS )
287+
288+ // Call the parent's resizeArtboard() which will check hasCppObject
289+ // (the fix) and return early if disposed, preventing a crash
290+ super .resizeArtboard()
291+ }
292+ }
293+
294+ latchingRenderer.make()
295+
296+ // Capture any exception thrown in the background thread
297+ val exceptionRef = AtomicReference <Throwable >()
298+
299+ // Start draw() in a background thread
300+ val drawThread = Thread {
301+ try {
302+ latchingRenderer.draw()
303+ } catch (e: Throwable ) {
304+ exceptionRef.set(e)
305+ }
306+ }
307+ drawThread.start()
308+
309+ // Wait for resizeArtboard() to be entered
310+ duringResizeLatch.await(timeout, TimeUnit .MILLISECONDS )
311+
312+ // Delete the renderer while resizeArtboard() is blocked
313+ latchingRenderer.delete()
314+
315+ // Let resizeArtboard() continue - the hasCppObject check should prevent a crash
316+ afterDeleteLatch.countDown()
317+
318+ drawThread.join(timeout)
319+
320+ // Verify no exception was thrown - the fix should prevent the crash
321+ val exception = exceptionRef.get()
322+ assert (exception == null ) {
323+ " Expected no exception when renderer is deleted during resizeArtboard(). " +
324+ " Got: ${exception?.javaClass?.simpleName} : ${exception?.message} "
325+ }
326+ }
258327}
0 commit comments