Search Docs
Loading...
Skip to content

Cancel Operations

Stop an export, a load or a save that your app no longer needs, or give it a deadline, by cancelling the coroutine that runs it.

Cancel Operations example showing a design that loaded before its deadline

8 mins
estimated time
GitHub

Exports, loads, downloads and saves go on in the engine while their suspend function waits. When a user closes a screen or picks another file, your app no longer needs that work. Each engine call that exports, loads, downloads or saves stops its work in the engine when you cancel its coroutine. The call then throws CancellationException, and the engine frees what the call holds.

This guide covers how to cancel a call, which calls you can cancel, how to give a call a deadline, how to cancel from a progress callback, and what the engine keeps after a cancel.

Using the Built-in UI#

The video export dialog of the Video Editor Starter Kit shows the progress of the export and a Close button. The button asks for a confirmation, then cancels the export. This guide covers the programmatic approach, which works the same in the editor and in a headless engine.

Cancelling a Call#

Launch the call in a coroutine scope that you own, such as lifecycleScope or viewModelScope, and keep the Job. Here we export a page that holds an image block. The image still downloads when the export starts, so the export has work to stop.

val export = launch {
try {
engine.block.export(block = page, mimeType = MimeType.PNG)
Log.i(TAG, "The export ended before the cancel")
} catch (e: CancellationException) {
Log.i(TAG, "The export stopped")
throw e
}
}

Call cancel() on the job when the user no longer needs the file, for example from a cancel button. The suspended call throws CancellationException, so the code after the call does not run. cancelAndJoin() also waits until the coroutine has ended.

export.cancelAndJoin()

The catch block logs the cancel and throws the exception again, so the coroutine ends as cancelled. A call that has already returned ignores the cancel. You can cancel from any thread, because the cancel reaches the engine on its own thread. A cancelled scope such as viewModelScope cancels every call that it runs.

Other suspend functions of the engine, such as engine.asset.findAssets(), also throw CancellationException when you cancel their coroutine. Their work in the engine runs to its end, and its result is dropped.

Calls That Can Be Cancelled#

These calls stop their work in the engine when you cancel their coroutine:

  • Exports: engine.block.export() for images and PDF, also into an OutputStream, engine.block.exportWithColorMask(), engine.block.exportVideo(). The PDF export as a Flow of chunks stops when you cancel the coroutine that collects it.
  • Scene and block loads: engine.scene.load() from a Uri, or from a string with waitForResources, engine.scene.loadArchive(), engine.scene.applyTemplate() from a Uri, engine.block.loadFromURL(), engine.block.loadFromArchive()
  • New scenes from media: engine.scene.createFromImage(), engine.scene.createFromVideo()
  • Downloads for a block or an asset: engine.block.createCaptionsFromURI(), engine.block.addImageFileUriToSourceSet(), engine.block.addVideoFileUriToSourceSet(), engine.block.forceLoadAVResource(), engine.block.forceLoadResources(), engine.asset.addLocalSourceFromJSON(), and engine.asset.applyAssetSourceAsset() and engine.asset.defaultApplyAsset() without a target block
  • Lookups: engine.editor.getMimeType(), engine.editor.getFontMetrics(), engine.block.getDominantColors()
  • Color profiles: engine.scene.setCMYKProfile(), engine.editor.loadCMYKProfile()
  • Saves: engine.scene.saveToArchive(), engine.block.saveToArchive()

engine.scene.saveToString() and engine.block.saveToString() have no callback that waits for your app, so they end within the call and have nothing to cancel. Android has no audio export.

Giving a Call a Deadline#

Wrap the call in withTimeout(). The engine has no timeout of its own, so the timeout is the deadline. At the deadline the call stops, and withTimeout() throws TimeoutCancellationException. Here we give the scene load ten seconds.

val sceneLoadedInTime = try {
withTimeout(10_000) {
engine.scene.load(sceneUri = sceneUri)
}
Log.i(TAG, "The scene loaded in time")
true
} catch (e: TimeoutCancellationException) {
Log.i(TAG, "The scene did not load within 10 seconds")
false
}

Catch TimeoutCancellationException to tell a missed deadline from a failed download. A failed download throws an EngineException, which this snippet does not catch. withTimeoutOrNull() returns null at the deadline instead of throwing.

Cancelling From a Progress Callback#

You can cancel the export coroutine inside progressCallback of exportVideo(). Here we stop a video export that has rendered less than half of its frames after one minute.

suspend fun exportVideoUnlessSlow(
engine: Engine,
page: DesignBlock,
): ByteBuffer? = coroutineScope {
val startedAt = SystemClock.elapsedRealtime()
val export = async {
engine.block.exportVideo(
block = page,
timeOffset = 0.0,
duration = engine.block.getDuration(page),
mimeType = MimeType.MP4,
progressCallback = { progress ->
val elapsedMillis = SystemClock.elapsedRealtime() - startedAt
// Less than half of the frames after one minute: the export takes too long.
if (elapsedMillis > 60_000 && progress.renderedFrames < progress.totalFrames / 2) {
this@async.cancel()
}
},
)
}
try {
export.await().also { video ->
Log.i(TAG, "The video file has ${video.remaining()} bytes")
}
} catch (e: CancellationException) {
// Throws again when the caller was cancelled, not the slow export.
ensureActive()
Log.i(TAG, "The export was too slow and stopped")
null
}
}

this@async.cancel() cancels only the export coroutine, not the caller. In the catch block, ensureActive() throws again when the caller itself was cancelled. Once exportVideo() has thrown, the callback gets no more reports, and the next export can start at once. The time of the first reports includes the start-up of the export, so an estimate of the total time from them would also stop fast exports.

What a Cancel Stops and What It Keeps#

A cancel stops the work of the call, but the engine keeps what other blocks and calls still need:

  • An export runs in a background engine. A cancel ends the export and stops that engine, which frees what the export holds. The next export starts a new background engine.
  • A load stops its download, unless a block or another call needs the same file. The engine reads every video fill at each update, so the download of a video fill goes on.
  • engine.scene.load() from a Uri, loadArchive(), applyTemplate(), createFromImage() and createFromVideo() keep the current scene. With waitForResources, load() from a Uri replaces the scene before the wait for its resources, so a cancel during that wait keeps the new scene. load() from a string with waitForResources loads the scene at once, so a cancel ends only the wait.
  • loadCMYKProfile() ends only the wait, because the document keeps its profile loaded.
  • A cancelled setCMYKProfile() gives back its claim, so an assignment that it replaced still takes effect.
  • applyAssetSourceAsset() stops the download that the default apply waits for, so it creates no block. An asset source with its own applyAsset keeps running in your code. A block that it creates stays, and the call does not return it.
  • getDominantColors() waits while a resource of the block loads, so withTimeout() gives it a deadline.

Starting the Next Call#

After the call throws, the engine is free for the next export or load. Start it after cancelAndJoin() returns, or in a new coroutine. Here we export the same page again, this time without a cancel.

val png = engine.block.export(block = page, mimeType = MimeType.PNG)
Log.i(TAG, "The next export made a PNG file of ${png.remaining()} bytes")

The second export waits for the image that the first export waited for, because the image block still needs it.

Troubleshooting#

  • The code after a cancelled call runs: A catch (e: Exception) around the call also catches CancellationException. Throw it again, or catch only the exceptions that you expect.
  • The call returns its result although you cancelled: The call ended before the cancel.
  • The coroutine ends without an error at the deadline: An uncaught TimeoutCancellationException ends the coroutine as cancelled. Catch it around withTimeout(), or use withTimeoutOrNull().
  • The download of a cancelled load goes on: A block on the canvas or another call needs the same file.

API Reference#

These are the methods this guide calls. The section Calls That Can Be Cancelled lists every method that stops its work in the engine on a cancel.

Method Category Purpose
engine.block.export(block=_, mimeType=_) Block Export a block as an image or a PDF
engine.block.exportVideo(block=_, timeOffset=_, duration=_, mimeType=_, progressCallback=_) Block Export a page as a video
engine.block.getDuration(block=_) Block Get the playback duration of a block
engine.scene.load(sceneUri=_) Scene Load a scene or an archive from a Uri

Next Steps#