In this example, we will show you how to integrate your custom asset sources into CE.SDK.
With CE.SDK you can directly add external image providers like Unsplash or your own backend. A third option we will explore in this guide is using the engine’s Asset API directly. Follow along with this example while we are going to add the Unsplash library.
Adding an asset source is done creating an asset source definition and adding it using fun addSource(source: AssetSource)
.
The asset source needs a unique identifier as part of an object implementing the interface of the source.
All Asset API methods require the asset source’s unique identifier.
val source = UnsplashAssetSource(unsplashBaseUrl) // INSERT YOUR UNSPLASH PROXY URL HEREengine.asset.addSource(source)
The most important function to implement is suspend fun findAssets(sourceId: String, query: FindAssetsQuery): FindAssetsResult
.
With this function alone you can define the complete asset source.
It receives the asset query as an argument and returns results asynchronously.
- The argument is the
query
and describes the slice of data the engine wants to use. This includes a query string and pagination information. - The result of this query, besides the actual asset data, returns information like the current page, the next page and the total number of assets available for this specific query.
Providing a suspend
function gives us great flexibility since we are completely agnostic of how we want to get the assets.
We can use HttpURLConnection
, local storage, cache or import a 3rd party library to return the result.
val list = runCatching { engine.asset.findAssets( sourceId = "ly.img.asset.source.unsplash", query = FindAssetsQuery(query = "", page = 1, perPage = 10), )}.getOrElse { it.printStackTrace() }
Let us implement an Unsplash asset source. Please note that this is just for demonstration purposes only and may not be ideal if you want to integrate Unsplash in your production environment.
We will create a class implementing abstract class AssetSource
. A unique sourceId = "ly.img.asset.source.unsplash"
is passed via the constructor.
There are multiple abstract methods that we need to implement, however, findAssets
is the most important one.
class UnsplashAssetSource( private val baseUrl: String,) : AssetSource(sourceId = "ly.img.asset.source.unsplash") {
findAssets
is the function that receives query
object from the engine and is supposed to return the corresponding results.
Unsplash has different API endpoints for different use cases. If we want to search we need to call a different endpoint as if we just want to display images without any search term.
Therefore we need to check if the query data contains a query
string. If findAssets
was called with a non-empty query
we call the /search
endpoint via getSearchList
function, otherwise we call getPopularList
. As we can see in the example, we are passing the query
object to findAssets
method, containing the following fields:
query.query
: The current search string from the search bar in the asset library.query.page
: For Unsplash specifically the requested page number starts with 1. We do not query all assets at once but by pages. As the user scrolls down more pages will be requested by calls to thefindAssets
method.query.perPage
: Determines how many assets we want to have included per page. This might change between calls. For instance,perPage
can be called with a small number to display a small preview, but with a higher number e.g. if we want to show more assets in a grid view.
override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { if (query.query.isNullOrEmpty()) query.getPopularList() else query.getSearchList() }
private suspend fun FindAssetsQuery.getPopularList(): FindAssetsResult { val queryParams = listOf( "order_by" to "popular", "page" to page + 1, "per_page" to perPage, ).joinToString(separator = "&") { (key, value) -> "$key=$value" } val assetsArray = getResponseAsString("$baseUrl/photos?$queryParams").let(::JSONArray) return FindAssetsResult( assets = (0 until assetsArray.length()).map { assetsArray.getJSONObject(it).toAsset() }, currentPage = page, nextPage = page + 1, total = Int.MAX_VALUE, ) }
private suspend fun FindAssetsQuery.getSearchList(): FindAssetsResult { val queryParams = listOf( "query" to query, "page" to page + 1, "per_page" to perPage, ).joinToString(separator = "&") { (key, value) -> "$key=$value" } val response = getResponseAsString("$baseUrl/search/photos?$queryParams").let(::JSONObject) val assetsArray = response.getJSONArray("results") val total = response.getInt("total") val totalPages = response.getInt("total_pages") return FindAssetsResult( assets = (0 until assetsArray.length()).map { assetsArray.getJSONObject(it).toAsset() }, currentPage = page, nextPage = if (page == totalPages) -1 else page + 1, total = total, ) }
Once we receive the response and check for success we need to map Unsplash’s result to what the asset source API needs as a result. The CE.SDK expects an object with the following properties:
assets
: An array of assets for the current query. We will take a look at what these have to look like in the next paragraph.currentPage
: Return the current page that was requested.nextPage
: This is the next page that can be requested after the current one. Should be-1
if there is no other page (no more assets). In this case we stop querying for more even if the user has scrolled to the bottom.total
: The total number of assets available for the current query. If we search for “Cat” withperPage
set to 30, we will get 30 assets, buttotal
likely will be a much higher number.
val response = getResponseAsString("$baseUrl/search/photos?$queryParams").let(::JSONObject)val assetsArray = response.getJSONArray("results")val total = response.getInt("total")val totalPages = response.getInt("total_pages")return FindAssetsResult( assets = (0 until assetsArray.length()).map { assetsArray.getJSONObject(it).toAsset() }, currentPage = page, nextPage = if (page == totalPages) -1 else page + 1, total = total,)
Every image we get as a result of Unsplash needs to be translated into an object that is expected by the asset source API. We will describe every mandatory and optional property in the following paragraphs.
private fun JSONObject.toAsset() = Asset( id = getString("id"), locale = "en", label = when { !isNull("description") -> getString("description") !isNull("alt_description") -> getString("alt_description") else -> null }, tags = takeIf { has("tags") }?.let { getJSONArray("tags") }?.let { (0 until it.length()).map { index -> it.getJSONObject(index).getString("title") } }?.takeIf { it.isNotEmpty() }, meta = mapOf( "uri" to getJSONObject("urls").getString("full"), "thumbUri" to getJSONObject("urls").getString("thumb"), "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "kind" to "image", "width" to getInt("width").toString(), "height" to getInt("height").toString(), ), context = AssetContext(sourceId = "unsplash"), credits = AssetCredits( name = getJSONObject("user").getString("name"), uri = getJSONObject("user") .takeIf { it.has("links") } ?.getJSONObject("links") ?.getString("html") ?.let { Uri.parse(it) }, ), utm = AssetUTM(source = "CE.SDK Demo", medium = "referral"),)
id
: The id of the asset (mandatory).
This has to be unique for this source configuration.
id = getString("id"),
locale
(optional): The language locale for this asset is used in label
and tags
.
locale = "en",
label
(optional): The label of this asset.
It could be displayed in the tooltip as well as in the credits of the asset.
label = when { !isNull("description") -> getString("description") !isNull("alt_description") -> getString("alt_description") else -> null},
tags
(optional): The tags of this asset.
It could be displayed in the credits of the asset.
tags = takeIf { has("tags") }?.let { getJSONArray("tags") }?.let { (0 until it.length()).map { index -> it.getJSONObject(index).getString("title") }}?.takeIf { it.isNotEmpty() },
meta
: The meta object stores asset properties that depend on the specific asset type.
meta = mapOf( "uri" to getJSONObject("urls").getString("full"), "thumbUri" to getJSONObject("urls").getString("thumb"), "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "kind" to "image", "width" to getInt("width").toString(), "height" to getInt("height").toString(),),
uri
: For an image asset this is the URL to the image file that will be used to add the image to the scene.
Note that we have to use the Unsplash API to obtain a usable URL at first.
"uri" to getJSONObject("urls").getString("full"),
thumbUri
: The URI of the asset’s thumbnail.
It could be used in an asset library.
"thumbUri" to getJSONObject("urls").getString("thumb"),
blockType
: The type id of the design block that should be created when this asset is applied to the scene.
If omitted, CE.SDK will try to infer the block type from an optionally provided mimeType
property (e.g. image/jpeg
) or by loading the asset data behind uri
and parsing the mime type from that. However, this will cause a delay before the asset can be added to the scene, which is why it is always recommended to specify the blockType
upfront.
"blockType" to DesignBlockType.Graphic.key,
fillType
: The type id of the fill that should be attached to the block when this asset is applied to the scene.
If omitted, CE.SDK will default to a solid color fill //ly.img.ubq/fill/color
.
"fillType" to FillType.Image.key,
shapeType
: The type id of the shape that should be attached to the block when this asset is applied to the scene.
If omitted, CE.SDK will default to a rect shape //ly.img.ubq/shape/rect
.
"shapeType" to ShapeType.Rect.key,
kind
: The kind that should be set to the block when this asset is applied to the scene.
If omitted, CE.SDK will default to an empty string.
"kind" to "image",
width
: The original width of the image.
height
: The original height of the image.
"width" to getInt("width").toString(),"height" to getInt("height").toString(),
looping
: Determines whether the asset allows looping (applicable only to Video and GIF). When set to true
, the asset can extend beyond its original length by looping for the specified duration.
context
: Adds contextual information to the asset.
Right now, this only includes the source id of the source configuration.
context = AssetContext(sourceId = "unsplash"),
credits
(optional): Some image providers require to display credits to the asset’s artist.
If set, it has to be an object with the artist’s name
and a url
to the artist page.
credits = AssetCredits( name = getJSONObject("user").getString("name"), uri = getJSONObject("user") .takeIf { it.has("links") } ?.getJSONObject("links") ?.getString("html") ?.let { Uri.parse(it) },),
utm
(optional): Some image providers require to add UTM parameters to all links to the source or the artist.
If set, it contains a string to the source
(added as utm_source
) and the medium
(added as utm_medium
)
utm = AssetUTM(source = "CE.SDK Demo", medium = "referral"),
After translating the asset to match the interface from the asset source API, the array of assets for the current page can be returned.
Going further with our Unsplash integration we need to handle the case when no query was provided.
Unsplash requires us to call a different API endpoint (/photos
) with slightly different parameters but the basics are the same.
We need to check for success, calculate total
and nextPage
and translate the assets.
val search = runCatching { engine.asset.findAssets( sourceId = "ly.img.asset.source.unsplash", query = FindAssetsQuery(query = "banana", page = 1, perPage = 10), )}.getOrElse { it.printStackTrace() }
In addition to findAssets
, there are couple more methods that need to be implemented.
We have already seen that an asset can define credits for the artist.
Depending on the image provider you might need to add credits and the license for the source.
In case of Unsplash, this includes a link as well as the license of all assets from this source.
override val credits = AssetCredits( name = "Unsplash", uri = Uri.parse("https://unsplash.com/"), )
override val license = AssetLicense( name = "Unsplash license (free)", uri = Uri.parse("https://unsplash.com/license"), )
Local Asset Sources
In many cases, you already have various finite sets of assets that you want to make available via asset sources. In order to save you the effort of having to implement custom asset query callbacks for each of these asset sources, CE.SDK also allows you to create “local” asset sources, which are managed by the engine and provide search and pagination functionalities.
In order to add such a local asset source, simply call the addLocalSource
API and choose a unique id with which you
can later access the asset source.
engine.asset.addLocalSource( sourceId = "background-videos", supportedMimeTypes = listOf("video/mp4"),)
The fun addAsset(sourceId: String, asset: AssetDefinition)
API allows you to add new asset instances to your local asset source. The local asset source then
keeps track of these assets and returns matching items as the result of asset queries. Asset queries return the
assets in the same order in which they were inserted into the local asset source.
Note that the AssetDefinition
type that we pass to the addAsset
API is slightly different than
the Asset
type which is returned by asset queries. The AssetDefinition
for example contains all localizations
of the labels and tags of the same asset whereas the Asset
is specific to the locale property of the query.
val asset = AssetDefinition( id = "ocean-waves-1", label = mapOf( "en" to "relaxing ocean waves", "es" to "olas del mar relajantes", ), tags = mapOf( "en" to listOf("ocean", "waves", "soothing", "slow"), "es" to listOf("mar", "olas", "calmante", "lento"), ), meta = mapOf( "uri" to "https://example.com/ocean-waves-1.mp4", "thumbUri" to "https://example.com/thumbnails/ocean-waves-1.jpg", "mimeType" to "video/mp4", "width" to "1920", "height" to "1080", ), payload = AssetPayload(color = AssetColor.RGB(r = 0F, g = 0F, b = 1F)),)engine.asset.addAsset(sourceId = "background-videos", asset = asset)
Full Code
Here’s the full code:
import android.net.Uriimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport ly.img.engine.Assetimport ly.img.engine.AssetColorimport ly.img.engine.AssetContextimport ly.img.engine.AssetCreditsimport ly.img.engine.AssetDefinitionimport ly.img.engine.AssetLicenseimport ly.img.engine.AssetPayloadimport ly.img.engine.AssetSourceimport ly.img.engine.AssetUTMimport ly.img.engine.DesignBlockTypeimport ly.img.engine.Engineimport ly.img.engine.FillTypeimport ly.img.engine.FindAssetsQueryimport ly.img.engine.FindAssetsResultimport ly.img.engine.ShapeTypeimport org.json.JSONArrayimport org.json.JSONObjectimport java.net.HttpURLConnectionimport java.net.URL
fun customAssetSource( license: String, userId: String, unsplashBaseUrl: String,) = CoroutineScope( Dispatchers.Main,).launch { val engine = Engine.getInstance(id = "ly.img.engine.example") engine.start(license = license, userId = userId) engine.bindOffscreen(width = 100, height = 100)
val source = UnsplashAssetSource(unsplashBaseUrl) // INSERT YOUR UNSPLASH PROXY URL HERE engine.asset.addSource(source)
val list = runCatching { engine.asset.findAssets( sourceId = "ly.img.asset.source.unsplash", query = FindAssetsQuery(query = "", page = 1, perPage = 10), ) }.getOrElse { it.printStackTrace() }
val search = runCatching { engine.asset.findAssets( sourceId = "ly.img.asset.source.unsplash", query = FindAssetsQuery(query = "banana", page = 1, perPage = 10), ) }.getOrElse { it.printStackTrace() }
engine.asset.addLocalSource( sourceId = "background-videos", supportedMimeTypes = listOf("video/mp4"), ) val asset = AssetDefinition( id = "ocean-waves-1", label = mapOf( "en" to "relaxing ocean waves", "es" to "olas del mar relajantes", ), tags = mapOf( "en" to listOf("ocean", "waves", "soothing", "slow"), "es" to listOf("mar", "olas", "calmante", "lento"), ), meta = mapOf( "uri" to "https://example.com/ocean-waves-1.mp4", "thumbUri" to "https://example.com/thumbnails/ocean-waves-1.jpg", "mimeType" to "video/mp4", "width" to "1920", "height" to "1080", ), payload = AssetPayload(color = AssetColor.RGB(r = 0F, g = 0F, b = 1F)), ) engine.asset.addAsset(sourceId = "background-videos", asset = asset)
engine.stop()}
class UnsplashAssetSource( private val baseUrl: String,) : AssetSource(sourceId = "ly.img.asset.source.unsplash") {
override suspend fun getGroups(): List<String>? = null
override val supportedMimeTypes = listOf("image/jpeg")
override val credits = AssetCredits( name = "Unsplash", uri = Uri.parse("https://unsplash.com/"), )
override val license = AssetLicense( name = "Unsplash license (free)", uri = Uri.parse("https://unsplash.com/license"), )
override suspend fun findAssets(query: FindAssetsQuery): FindAssetsResult = withContext(Dispatchers.IO) { if (query.query.isNullOrEmpty()) query.getPopularList() else query.getSearchList() }
private suspend fun FindAssetsQuery.getPopularList(): FindAssetsResult { val queryParams = listOf( "order_by" to "popular", "page" to page + 1, "per_page" to perPage, ).joinToString(separator = "&") { (key, value) -> "$key=$value" } val assetsArray = getResponseAsString("$baseUrl/photos?$queryParams").let(::JSONArray) return FindAssetsResult( assets = (0 until assetsArray.length()).map { assetsArray.getJSONObject(it).toAsset() }, currentPage = page, nextPage = page + 1, total = Int.MAX_VALUE, ) }
private suspend fun FindAssetsQuery.getSearchList(): FindAssetsResult { val queryParams = listOf( "query" to query, "page" to page + 1, "per_page" to perPage, ).joinToString(separator = "&") { (key, value) -> "$key=$value" } val response = getResponseAsString("$baseUrl/search/photos?$queryParams").let(::JSONObject) val assetsArray = response.getJSONArray("results") val total = response.getInt("total") val totalPages = response.getInt("total_pages") return FindAssetsResult( assets = (0 until assetsArray.length()).map { assetsArray.getJSONObject(it).toAsset() }, currentPage = page, nextPage = if (page == totalPages) -1 else page + 1, total = total, ) }
private suspend fun getResponseAsString(url: String) = withContext(Dispatchers.IO) { val connection = URL(url).openConnection() as HttpURLConnection require(connection.responseCode in 200 until 300) { connection.errorStream.bufferedReader().use { it.readText() } } connection.inputStream.bufferedReader().use { it.readText() } }
private fun JSONObject.toAsset() = Asset( id = getString("id"), locale = "en", label = when { !isNull("description") -> getString("description") !isNull("alt_description") -> getString("alt_description") else -> null }, tags = takeIf { has("tags") }?.let { getJSONArray("tags") }?.let { (0 until it.length()).map { index -> it.getJSONObject(index).getString("title") } }?.takeIf { it.isNotEmpty() }, meta = mapOf( "uri" to getJSONObject("urls").getString("full"), "thumbUri" to getJSONObject("urls").getString("thumb"), "blockType" to DesignBlockType.Graphic.key, "fillType" to FillType.Image.key, "shapeType" to ShapeType.Rect.key, "kind" to "image", "width" to getInt("width").toString(), "height" to getInt("height").toString(), ), context = AssetContext(sourceId = "unsplash"), credits = AssetCredits( name = getJSONObject("user").getString("name"), uri = getJSONObject("user") .takeIf { it.has("links") } ?.getJSONObject("links") ?.getString("html") ?.let { Uri.parse(it) }, ), utm = AssetUTM(source = "CE.SDK Demo", medium = "referral"), )}