Search Docs
Loading...
Skip to content

Import Local Asset

Register a local asset source, add a file Uri from the user’s device, and optionally insert the imported asset into the active scene.

5 mins
estimated time
GitHub

Local asset sources are useful when your app already has access to files on the device. The file can come from Android’s system document picker, your app’s private storage, or a file that your app copied into its sandbox.

This guide starts from a Uri that your app already resolved. If you receive a content:// URI from ActivityResultContracts.OpenDocument and need access after the current session, call takePersistableUriPermission in your app before handing the URI to CE.SDK.

Register a Local Source#

Create one stable source ID for the imported files. The example registers the source only once, then stores the supported MIME types on the source so later validation uses the same allow list.

if (sourceId !in engine.asset.findAllSources()) {
engine.asset.addLocalSource(
sourceId = sourceId,
supportedMimeTypes = LOCAL_ASSET_MIME_TYPES,
)
}

Use a dedicated source ID for each logical bucket of app-managed files. For example, keep user documents separate from bundled brand assets when they should appear in different library sections.

Validate the Picked File#

Detect the MIME type through the engine and compare it with the source’s supported types before adding the asset. This catches unsupported files before the asset library or apply flow tries to use them.

val mimeType = engine.editor.getMimeType(uri = fileUri)
val supportedMimeTypes = engine.asset.getSourceSupportedMimeTypes(sourceId = sourceId)
val acceptsMimeType = supportedMimeTypes.isEmpty() ||
supportedMimeTypes.any { supportedMimeType ->
supportedMimeType == "*/*" ||
supportedMimeType == mimeType ||
(supportedMimeType.endsWith("/*") && mimeType.startsWith(supportedMimeType.removeSuffix("*")))
}
require(acceptsMimeType) {
"Unsupported local asset MIME type: $mimeType"
}

For gallery-wide imports, use the Android photo-roll flow instead of this local-source pattern. For files that must move to backend storage before use, run the upload first and register the permanent URI.

Read Media Dimensions#

For image and video assets, read the intrinsic dimensions before creating the asset definition. CE.SDK uses this metadata when it creates the visual block, so non-square media keeps its aspect ratio when inserted.

data class LocalAssetDimensions(
val width: Int,
val height: Int,
)
fun readLocalAssetDimensions(
context: Context,
fileUri: Uri,
mimeType: String,
): LocalAssetDimensions? = when {
mimeType.startsWith("image/") -> readImageDimensions(context, fileUri)
mimeType.startsWith("video/") -> readVideoDimensions(context, fileUri)
else -> null
}
private fun readImageDimensions(
context: Context,
fileUri: Uri,
): LocalAssetDimensions {
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
when (fileUri.scheme) {
"file" -> {
val file = File(requireNotNull(fileUri.path) { "File URI has no path: $fileUri" })
file.inputStream().use { input ->
BitmapFactory.decodeStream(input, null, options)
}
}
else -> {
context.contentResolver.openInputStream(fileUri)?.use { input ->
BitmapFactory.decodeStream(input, null, options)
} ?: error("Could not open local image: $fileUri")
}
}
return requireDimensions(
width = options.outWidth,
height = options.outHeight,
fileUri = fileUri,
)
}
private fun readVideoDimensions(
context: Context,
fileUri: Uri,
): LocalAssetDimensions {
val retriever = MediaMetadataRetriever()
return try {
retriever.setDataSource(context, fileUri)
val width = retriever
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
?.toIntOrNull()
val height = retriever
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
?.toIntOrNull()
val rotation = retriever
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
?.toIntOrNull()
val dimensions = requireDimensions(
width = width,
height = height,
fileUri = fileUri,
)
if (rotation == 90 || rotation == 270) {
LocalAssetDimensions(width = dimensions.height, height = dimensions.width)
} else {
dimensions
}
} finally {
runCatching { retriever.release() }
}
}
private fun requireDimensions(
width: Int?,
height: Int?,
fileUri: Uri,
): LocalAssetDimensions {
require(width != null && width > 0 && height != null && height > 0) {
"Could not read dimensions for local asset: $fileUri"
}
return LocalAssetDimensions(width = width, height = height)
}

Build the Asset Definition#

An AssetDefinition stores the URI and the metadata CE.SDK needs to present and apply the asset. The sample supports images, videos, and audio; image and video assets use typed block and fill constants plus width and height metadata, while audio assets use the audio block type. For videos, pass the clip duration in seconds so the inserted block matches the local file length.

fun createLocalAssetDefinition(
context: Context,
fileUri: Uri,
displayName: String,
mimeType: String,
thumbnailUri: Uri? = null,
videoDurationSeconds: Double? = null,
dimensions: LocalAssetDimensions? = null,
): AssetDefinition {
val metadata = mutableMapOf(
"uri" to fileUri.toString(),
"mimeType" to mimeType,
)
val mediaDimensions = dimensions ?: readLocalAssetDimensions(
context = context,
fileUri = fileUri,
mimeType = mimeType,
)
mediaDimensions?.let {
metadata["width"] = it.width.toString()
metadata["height"] = it.height.toString()
}
when {
mimeType.startsWith("image/") -> {
metadata["kind"] = "image"
metadata["thumbUri"] = (thumbnailUri ?: fileUri).toString()
metadata["blockType"] = DesignBlockType.Graphic.key
metadata["fillType"] = FillType.Image.key
metadata["shapeType"] = ShapeType.Rect.key
}
mimeType.startsWith("video/") -> {
val duration = requireNotNull(videoDurationSeconds) {
"Video assets require duration metadata in seconds."
}
metadata["kind"] = "video"
thumbnailUri?.let { metadata["thumbUri"] = it.toString() }
metadata["duration"] = duration.toString()
metadata["blockType"] = DesignBlockType.Graphic.key
metadata["fillType"] = FillType.Video.key
metadata["shapeType"] = ShapeType.Rect.key
}
mimeType.startsWith("audio/") -> {
metadata["kind"] = "audio"
metadata["blockType"] = DesignBlockType.Audio.key
}
else -> error("Unsupported local asset MIME type: $mimeType")
}
return AssetDefinition(
id = UUID.randomUUID().toString(),
label = mapOf("en" to displayName),
tags = mapOf("en" to listOf("local", "device")),
meta = metadata,
)
}

A uri is required for these media assets. The sample also stores the detected mimeType, avoiding another MIME lookup during insertion. Add intrinsic dimensions for visual assets. Include user-facing labels and tags so asset search can find the item later. For videos shown in a visual grid, pass a generated poster image as thumbnailUri.

Add the Asset#

Add the definition to the local source. addAsset indexes the asset and notifies subscribers, so any asset library or custom UI displaying the source can refresh automatically.

engine.asset.addAsset(sourceId = sourceId, asset = asset)

Call assetSourceContentsChanged only after an out-of-band change that the engine cannot observe, such as replacing files in a custom source’s backing store without calling addAsset or removeAsset.

To expose this source in the CE.SDK editor UI, include an AssetSourceType(sourceId) entry in the relevant asset library category. The Asset Library guide covers how to configure the visible library sections.

Insert the Asset Immediately#

If your import button should place the file on the canvas right away, fetch the imported asset by its generated ID and pass it to defaultApplyAsset. Skip this step when you only want the file to appear in the asset library for later selection.

val importedAsset = engine.asset.fetchAsset(
sourceId = sourceId,
assetId = asset.id,
) ?: error("Could not fetch imported local asset: ${asset.id}")
val insertedBlock = engine.asset.defaultApplyAsset(asset = importedAsset)

defaultApplyAsset returns the new block when the asset type can be inserted into the active scene. Make sure an active scene exists before calling it.

Troubleshooting#

Issue Cause Solution
CE.SDK cannot read a picked file The app no longer has permission to access its content:// URI Persist the read permission returned by ActivityResultContracts.OpenDocument, or copy the file into app-managed storage
An inserted image or video has the wrong aspect ratio Its AssetDefinition is missing valid width and height metadata Read the intrinsic dimensions before adding the asset and store both values in meta
A video import fails while building the definition The sample did not receive the clip duration Read the duration from the local file and pass it in seconds as videoDurationSeconds
Adding an asset fails with a duplicate-ID error The source already contains an asset with the generated ID Generate a unique ID for each definition, or remove the existing asset before adding it again

API Reference#

Method Purpose
engine.asset.findAllSources() Check whether the local source is already registered
engine.asset.addLocalSource(sourceId=_, supportedMimeTypes=_) Register a mutable local asset source
engine.editor.getMimeType(uri=_) Resolve the MIME type for a local Uri
engine.asset.getSourceSupportedMimeTypes(sourceId=_) Read the MIME allow list stored on the source
engine.asset.addAsset(sourceId=_, asset=_) Add an AssetDefinition to a local source
engine.asset.assetSourceContentsChanged(sourceId=_) Notify subscribers after an out-of-band source change
engine.asset.fetchAsset(sourceId=_, assetId=_) Fetch the newly added asset by its generated ID
engine.asset.defaultApplyAsset(asset=_) Insert the asset into the active scene using CE.SDK’s default behavior

Key Types#

Type Purpose
AssetDefinition Stores the asset ID, localized labels and tags, and metadata such as uri, mimeType, width, height, duration, kind, and block creation hints
DesignBlockType Supplies typed block keys for graphic and audio assets
FillType Supplies typed fill keys for image and video assets
ShapeType Supplies the typed rectangle shape used for visual assets

Next Steps#