Search Docs
Loading...
Skip to content

Content Moderation Editor in Android

Add automatic content moderation to your Android editor—check the images and the text in a scene against a moderation service and flag content that might violate your guidelines, right on the mobile device.

Content Moderation Editor starter kit screenshot

10 mins
estimated time
Download
GitHub

Pre-Requisites#

This guide assumes basic familiarity with Android and Kotlin. You will need:

  • Latest Android Studio
  • Kotlin: 1.9.10 or later
  • Gradle: 8.4 or later
  • Android: 7.0+ (API level 24+)
  • An IMG.LY Gateway API key, used to moderate the images and text in the scene

Set Your Gateway API Key#

The kit moderates images and text with the imgly/detection model on the IMG.LY Gateway. Keys use the sk_ prefix, and model access and credit budgets are configured per key in the Dashboard, so the key you use must have access to imgly/detection and a credit balance to spend.

Paste the key into GATEWAY_API_KEY:

starter-kit/src/main/kotlin/ly/img/editor/configuration/contentmoderation/moderation/GatewayModerationProvider.kt
const val GATEWAY_API_KEY = "sk_live_..."

Every screen in the kit reads the key from this one constant. The Gateway API key is separate from your CE.SDK license key, so set both. Until the key is set, running a check reports a missing-key error instead of results, and the moderation sheet surfaces a rejected key or an empty balance there too.

Get Started#

Start with a complete, runnable Android starter kit project.

Step 1: Clone the Repository#

Terminal window
git clone -b v1.84.0-rc.1 https://github.com/imgly/starterkit-content-moderation-editor-android.git
cd starterkit-content-moderation-editor-android

Step 2: Open and Run#

Create and launch a new android emulator or use an existing one or connect a physical device with USB Debugging on.

Open the project in Android Studio, sync gradle via File -> Sync Project With Gradle Files and run the app module from UI, or use:

Terminal window
./gradlew app:installDebug

The sample app launches MainActivity that has “Launch Editor” button. Clicking it launches EditorActivity:

starter-kit/src/main/kotlin/ly/img/editor/configuration/contentmoderation/EditorActivity.kt
package ly.img.editor.configuration.contentmoderation
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import ly.img.editor.Editor
import ly.img.editor.configuration.contentmoderation.moderation.GATEWAY_API_KEY
import ly.img.editor.configuration.contentmoderation.moderation.GatewayModerationProvider
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
/**
* Encapsulated editor to be used in legacy activity navigation.
* Delete this file if you are using jetpack compose navigation.
*/
class EditorActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// This is required to remove the default action bar on top.
setTheme(android.R.style.Theme_Material_Light_NoActionBar)
// This is required, so that the editor is displayed full screen on relatively older devices.
enableEdgeToEdge()
setContent {
Editor(
license = null, // pass null or empty for evaluation mode with watermark
configuration = {
EditorConfiguration.remember(
builderFactory = {
// The content moderation backend lives behind the [ContentModerationProvider]
// interface. Replace [GatewayModerationProvider] with your own
// implementation to plug in a different moderation service.
ContentModerationConfigurationBuilder(
moderationProvider = GatewayModerationProvider(apiKey = GATEWAY_API_KEY),
)
},
)
},
onClose = {
// Finish the activity, potentially handle errors.
finish()
},
)
}
}
}

The full implementation of the starter kit lives in the starter-kit/ folder:

starter-kit/
├── build.gradle.kts # Starter kit library module config, includes ly.img:editor dependency
└── src/main/
├── AndroidManifest.xml # Starter kit manifest file, may contain permissions
├── assets/
│ └── scene/
│ └── content-moderation.scene # Default scene with remote images that should be loaded
└── kotlin/ly/img/editor/configuration/contentmoderation/
├── ContentModerationConfigurationBuilder.kt # Editor configuration logic encapsulated in 1 place
├── EditorActivity.kt # Encapsulated editor for legacy navigation. Delete if you use jetpack compose navigation
├── callback/
│ ├── OnCreate.kt # Editor initialization logic
│ ├── OnExport.kt # Export flow, including the moderation warning
│ └── OnLoaded.kt # Post onCreate logic
├── component/
│ ├── CanvasMenu.kt # Canvas Menu component configuration
│ ├── Dock.kt # Dock component configuration
│ ├── InspectorBar.kt # Inspector Bar component configuration
│ ├── ModerationSheet.kt # Bottom sheet that lists the moderation results
│ ├── NavigationBar.kt # Navigation Bar component configuration, including the moderation button
│ └── Overlay.kt # Overlay component configuration, including the export warning dialog
├── iconPack/
│ ├── IconPack.kt # Icon pack accessor for the starter kit
│ ├── ShieldCheck.kt # Moderation button (clean) and moderation sheet shield
│ ├── ShieldCheckFilled.kt # Moderation button (clean) while the sheet is open
│ ├── ShieldAlert.kt # Moderation button when content is flagged
│ ├── ShieldAlertFilled.kt # Moderation button (flagged) while the sheet is open
│ └── WarningTriangle.kt # Export warning dialog icon
└── moderation/
├── ContentModerationProvider.kt # Swappable interface for the moderation backend
├── ModerationEngine.kt # Scans image and text blocks and runs them through the provider
├── ModerationModels.kt # Category, state and result data types
└── GatewayModerationProvider.kt # Default provider talking to the IMG.LY gateway

Starter Kit as a Dynamic Feature#

Since starter-kit folder is an android library module, it is possible to turn it into a dynamic feature. This can be helpful if you want to lazy load the editor in order to reduce the download size of your app.

See Bundle Size for more details.

Configuring the Starter Kit#

The starter kit that we provide contains a very generic structure and behavior, however we understand that every customer wants to configure it according to their needs. The good thing is that the starter kit implementation is part of your codebase and you can configure, add/remove/modify functionality as you wish. In addition, you may want to configure the editor based on your business logic, i.e. restore the scene file from previous edits, display different dock items for different users etc.

This example demonstrates on how to pass, store and use external parameters in the starter kit. First, declare a new property to the builder class:

...
import android.net.Uri
@Stable
class ContentModerationConfigurationBuilder(
val moderationProvider: ContentModerationProvider,
) : BasicConfigurationBuilder() {
/**
* The scene uri that should be loaded in onCreate if not null.
* Note that editorContext.mutableStateOf is used to store mutable objects in the editor scope that survive configuration changes.
*/
var sceneUri: Uri? by editorContext.mutableStateOf(
key = "your.package.name.state.sceneUri",
initial = null,
)
...
}

Next, read your external parameter from activity intent extras (or jetpack compose screen arguments) and assign to the property of the builder:

import androidx.core.net.toUri
class EditorActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
val sceneUri = intent.getStringExtra("sceneUri")?.toUri()
Editor(
license = null, // pass null or empty for evaluation mode with watermark
configuration = {
EditorConfiguration.remember(
builderFactory = {
ContentModerationConfigurationBuilder(
moderationProvider = GatewayModerationProvider(apiKey = GATEWAY_API_KEY),
)
},
) {
this.sceneUri = sceneUri
}
},
onClose = {
// Finish the activity, ignore any errors.
finish()
},
)
}
}

Finally, make use of the property. The scene loading logic is located at OnCreate.kt file, as part of onCreate implementation (see next section for more details). We modify this function to load the sceneUri instead if the value is not null:

starter-kit/src/main/kotlin/ly/img/editor/configuration/contentmoderation/callback/OnCreate.kt
suspend fun ContentModerationConfigurationBuilder.onCreateScene() {
sceneUri?.let { safeSceneUri ->
// Load the sceneUri if it's not null
getOrLoadScene(sceneUri = safeSceneUri)
} ?: run {
// Otherwise stick to the default behavior of the starter kit
getOrLoadScene(sceneUri = "file:///android_asset/scene/content-moderation.scene".toUri())
}
}

Set Up a Scene#

The scene setup logic is located at OnCreate.kt file, as part of onCreate implementation:

starter-kit/src/main/kotlin/ly/img/editor/configuration/contentmoderation/callback/OnCreate.kt
suspend fun ContentModerationConfigurationBuilder.onCreateScene() {
getOrLoadScene(sceneUri = "file:///android_asset/scene/content-moderation.scene".toUri())
}

getOrLoadScene is a helper that loads an existing scene if available, or initializes one from file:///android_asset/scene/content-moderation.scene when no scene is active. The bundled scene references remote images so the moderation service, which fetches images by URL, has content it can check out of the box.

CE.SDK offers multiple ways to load scene into the editor. Choose the method that matches your use case:

// Load from an image uri - creates a new scene with the image
editorContext.engine.scene.createFromImage(imageUri = "https://example.com/photo.jpg".toUri())
// Load from a template archive - restores a previously saved project
editorContext.engine.scene.load(sceneUri = "https://example.com/template.zip".toUri())
// Create a blank canvas - starts with an empty design scene
editorContext.engine.scene.create()
// Load from a scene file - restores a scene from .scene file
editorContext.engine.scene.load(sceneUri = "https://example.com/saved.scene".toUri())

Customize Assets#

The asset source setup is located in OnCreate.kt as part of onCreate implementation. Enable or disable individual sources:

starter-kit/src/main/kotlin/ly/img/editor/configuration/contentmoderation/callback/OnCreate.kt
suspend fun ContentModerationConfigurationBuilder.onLoadAssetSources() {
// Load asset sources in parallel from content.json files
coroutineScope {
val baseUri = editorContext.baseUri
val sourceIds = listOf(
"ly.img.sticker",
"ly.img.vector.shape",
"ly.img.filter",
"ly.img.color.palette",
"ly.img.effect",
"ly.img.blur",
"ly.img.typeface",
"ly.img.crop.presets",
"ly.img.page.presets",
"ly.img.text",
"ly.img.text.styles",
"ly.img.text.curves",
"ly.img.text.components",
"ly.img.image",
)
sourceIds.forEach { id ->
launch {
editorContext.engine.asset.addLocalSourceFromJSON(
contentUri = "$baseUri/$id/content.json".toUri(),
)
}
}
}
// Load local asset sources
editorContext.engine.asset.addLocalSource(
sourceId = "ly.img.image.upload",
supportedMimeTypes = listOf(
"image/jpeg",
"image/png",
"image/heic",
"image/heif",
"image/svg+xml",
"image/gif",
"image/apng",
"image/bmp",
),
)
// Register gallery asset sources
listOf(
AssetSourceType.GalleryAllVisuals,
AssetSourceType.GalleryImage,
AssetSourceType.GalleryVideo,
).forEach { type ->
editorContext.engine.asset.addSource(
source = SystemGalleryAssetSource(
context = editorContext.engine.applicationContext,
type = type,
),
)
}
SystemGalleryPermission.setMode(systemGalleryConfiguration)
}

For production deployments, self-hosting assets is required—the IMG.LY CDN is intended for development only. See Serve Assets for downloading assets, configuring baseUri and excluding unused sources to optimize load times.

Customize Export Functionality#

Export handling logic is located in OnExport.kt as part of onExport callback.

onExportByteBuffer controls what should be exported from the scene. It can be a scene or set of design blocks. For this editor, it makes sense to export the scene to a PDF content. export is a helper function that calls editorContext.engine.block.export under the hood:

starter-kit/src/main/kotlin/ly/img/editor/configuration/contentmoderation/callback/OnExport.kt
suspend fun ContentModerationConfigurationBuilder.onExportByteBuffer(): ByteBuffer = export(
block = requireNotNull(editorContext.engine.scene.get()),
mimeType = MimeType.PDF,
)

onPostExport controls what should happen to the exported ByteBuffer content. You can upload the result to your server, save it to the device gallery or simply close the editor via editorContext.eventHandler.send(EditorEvent.CloseEditor()). Check writeToFile, shareFile and shareUri helper functions for potential implementations:

starter-kit/src/main/kotlin/ly/img/editor/configuration/contentmoderation/callback/OnExport.kt
suspend fun ContentModerationConfigurationBuilder.onPostExport(byteBuffer: ByteBuffer) {
val file = writeToFile(byteBuffer = byteBuffer, mimeType = MimeType.PDF)
shareFile(file = file, mimeType = MimeType.PDF)
}

Content Moderation#

The kit adds a Content Moderation button to the navigation bar — a shield-check that becomes a shield-alert once a check flags content, and stays an alert across edits until a later check comes back clean. The icon fills while the sheet is open, and tapping it again closes the sheet. Tapping it opens a bottom sheet that first prompts the user to Run Moderation; the sheet then shows a loading state and finally the flagged categories. When nothing is flagged, the sheet says so and the user is good to go. Results are grouped by block: each flagged block gets one heading and one “Select” action that jumps to it on the canvas, with its categories listed underneath, each carrying a severity dot — red for Certain, orange for Likely. A block that violates several categories at once therefore takes one row, not several. The header counts both, for example 6 issues · 4 blocks. Exporting is gated too: the export warning is skipped only when a fresh check of the unchanged scene found nothing; otherwise a confirmation dialog asks whether to export anyway — because the last check flagged content, or because the scene has unchecked changes (a freshly loaded scene is never assumed to be safe).

The navigation bar button is configured in NavigationBar.kt and sends an event that opens the moderation sheet:

starter-kit/src/main/kotlin/ly/img/editor/configuration/contentmoderation/component/NavigationBar.kt
/**
* A navigation bar button that runs content moderation on the scene and opens the results bottom sheet.
*
* @param builder the configuration builder, used to reach the [ContentModerationProvider].
*/
@Composable
fun NavigationBar.Button.rememberModerate(builder: ContentModerationConfigurationBuilder): Button<NavigationBar.ItemScope> =
NavigationBar.Button.remember {
id = { EditorComponentId("ly.img.contentmoderation.navigationBar.button.moderate") }
// Reflect the last completed check (a warning persists across edits until a clean
// re-check), and fill the icon while the moderation sheet is open.
vectorIcon = {
val flagged = builder.lastModerationHadIssues
val open = builder.isModerationSheetOpen
when {
flagged && open -> IconPack.ShieldAlertFilled
flagged -> IconPack.ShieldAlert
open -> IconPack.ShieldCheckFilled
else -> IconPack.ShieldCheck
}
}
contentDescription = { "Content moderation" }
onClick = {
// Toggle: tapping the icon while the sheet is open closes it.
if (builder.isModerationSheetOpen) {
editorContext.eventHandler.send(EditorEvent.Sheet.Close(animate = true))
} else {
editorContext.eventHandler.send(createModerationSheetOpenEvent(builder = builder))
}
}
}

Every image block and every text block in the scene is checked. A fill that already points at a public http(s) URL is handed to the service as that URL, so it fetches the image itself. Every other fill—a gallery upload, a bundled asset—is rendered by the engine and uploaded first, so user-added photos are covered too. Text blocks are sent as their text; blank ones are skipped.

Rendered images are capped at a 1024-pixel target, because the classifier does not need the full resolution of a 12-megapixel gallery photo and the upload runs over a phone connection. If you would rather send the image as-is, drop targetWidth/targetHeight from the ExportOptions in ModerationEngine.kt. Note that the engine renders the block large enough to fill the target, so a small block is scaled up to it rather than down. When a check cannot run—for example the service is unavailable or no API key is set—the sheet shows a clear error state instead of any results.

A text result is labelled with a short quote of the offending text rather than the block name, because a text block’s name defaults to “Text” and would not tell the user which caption was flagged. The bundled demo scene ships two captions written to trip the text categories, so the feature is exercisable out of the box.

Content Categories#

The imgly/detection model scores images and text against two separate category sets. The kit maps each score to a severity with the same two thresholds, both constructor parameters on GatewayModerationProvider: above 80% is reported as Certain, above 40% as Likely.

Images:

Category Covers
Nudity Raw or partial nudity, sexual acts, and suggestive content
Weapons Handguns, rifles, machine guns, threatening knives
Recreational Drugs Cannabis and other recreational drugs
Medical Pills, syringes, medical devices, and other medical imagery
Alcohol Wine, beer, cocktails, champagne
Offensive Symbols Hate symbols, extremist imagery, and offensive gestures
Gore Blood, wounds, corpses, and other graphic imagery

Text:

Category Covers
Toxic Language Language likely to make someone leave the conversation
Insulting Language Insults, name-calling, and demeaning remarks
Discriminatory Language Language that demeans a group or protected characteristic
Violent Language Threats of violence and calls to harm someone
Sexual Language Sexually explicit or suggestive wording
Self-Harm References to suicide, self-injury, or eating disorders

The two sets are disjoint, so one lookup table in the provider covers both. A category the model scores that the provider has no copy for is still reported, named from its id, so a category added to the model later is never silently dropped.

To save on API usage, the results are cached: running it again without editing the scene reuses the previous results instead of calling the service, and exporting an unchanged scene that a fresh check found clean skips the warning. Any undo, redo or edit invalidates the cache, so the next check reflects the current content and export warns again until it is re-checked.

A block whose request fails is reported as unchecked instead of being dropped, so a partial run shows as Incomplete check and names how many blocks could not be checked. An incomplete check is never cached, so the export warning is never skipped until a run covers the whole scene.

A check keeps at most four requests in flight at a time, otherwise a scene with many text blocks opens one connection per block at once and the service rate-limits it.

Use Your Own Moderation Service#

The moderation backend sits behind a single ContentModerationProvider interface. The default GatewayModerationProvider calls the imgly/detection model on the IMG.LY gateway, which scores images and text against separate category sets (see Content Categories). A category the model scores but the provider has no copy for is still reported, named from its id, so a category added to the model later is never silently dropped. To use a different service, plug in your own implementation:

starter-kit/src/main/kotlin/ly/img/editor/configuration/contentmoderation/moderation/ContentModerationProvider.kt
fun interface ContentModerationProvider {
/**
* Classifies a single image or piece of text into moderation categories.
*
* The content arrives as a [ModerationContent.ImageUrl] the service can fetch itself, as
* [ModerationContent.ImageBytes] when the fill is local, or as [ModerationContent.Text] for a
* text block. Implementations decide their own categories, descriptions and thresholds.
*
* @param content the image or text to classify.
* @return the list of categories with the [ModerationState] assigned to each.
*/
suspend fun moderate(content: ModerationContent): List<ModerationCategory>
}

Implement the interface with your backend and pass it to the builder in EditorActivity—nothing else needs to change, since the scene scan and every UI component depend only on the interface:

import ly.img.editor.configuration.contentmoderation.moderation.ContentModerationProvider
import ly.img.editor.configuration.contentmoderation.moderation.ModerationCategory
import ly.img.editor.configuration.contentmoderation.moderation.ModerationContent
import ly.img.editor.configuration.contentmoderation.moderation.ModerationState
class MyModerationProvider : ContentModerationProvider {
override suspend fun moderate(content: ModerationContent): List<ModerationCategory> {
// Call your own moderation service and map the response to categories.
val response = when (content) {
is ModerationContent.ImageUrl -> myBackend.classifyUrl(content.url)
is ModerationContent.ImageBytes -> myBackend.classifyBytes(content.bytes, content.mimeType)
is ModerationContent.Text -> myBackend.classifyText(content.text)
}
return listOf(
ModerationCategory(
id = "weapon",
displayName = "Weapons",
description = "Handguns, rifles, machine guns, threatening knives...",
state = if (response.weapon > 0.8) ModerationState.FAILED else ModerationState.SUCCESS,
),
// ... your remaining categories
)
}
}
// In EditorActivity:
EditorConfiguration.remember(
builderFactory = { ContentModerationConfigurationBuilder(moderationProvider = MyModerationProvider()) },
)

A custom provider fully controls its own categories, descriptions and thresholds—the results sheet simply renders whatever it returns. The content it receives is a ModerationContent.ImageUrl the service can fetch itself, ModerationContent.ImageBytes for a fill with no public URL, or ModerationContent.Text for a text block.


Customize (Optional)#

Base Uri#

The starter kit does not make any baseUri configuration, which means it points to https://cdn.img.ly/packages/imgly/cesdk-engine/1.84.0-rc.1/assets. If you want to store them in your own CDN or locally, assets can be accessed via zip file. For example, if you want to store them locally, unzip the content and place at starter-kit/src/main/assets:

import androidx.compose.runtime.Composable
import androidx.core.net.toUri
import ly.img.editor.Editor
import ly.img.editor.configuration.contentmoderation.ContentModerationConfigurationBuilder
import ly.img.editor.configuration.contentmoderation.moderation.GATEWAY_API_KEY
import ly.img.editor.configuration.contentmoderation.moderation.GatewayModerationProvider
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
@Composable
fun EditorBaseUriScreen(onClose: (error: Throwable?) -> Unit) {
Editor(
license = null, // pass null or empty for evaluation mode with watermark
baseUri = "file:///android_asset".toUri(), // this points to android assets
configuration = {
EditorConfiguration.remember(
builderFactory = {
ContentModerationConfigurationBuilder(
moderationProvider = GatewayModerationProvider(apiKey = GATEWAY_API_KEY),
)
},
)
},
onClose = onClose,
)
}

UI Mode#

CE.SDK supports light and dark ui modes out of the box, plus automatic system preference detection. Switch between themes programmatically:

import androidx.compose.runtime.Composable
import ly.img.editor.Editor
import ly.img.editor.EditorUiMode
import ly.img.editor.configuration.contentmoderation.ContentModerationConfigurationBuilder
import ly.img.editor.configuration.contentmoderation.moderation.GATEWAY_API_KEY
import ly.img.editor.configuration.contentmoderation.moderation.GatewayModerationProvider
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
@Composable
fun EditorUiModeScreen(onClose: (error: Throwable?) -> Unit) {
Editor(
license = null, // pass null or empty for evaluation mode with watermark
uiMode = EditorUiMode.SYSTEM, // EditorUiMode.SYSTEM, EditorUiMode.LIGHT, EditorUiMode.DARK
configuration = {
EditorConfiguration.remember(
builderFactory = {
ContentModerationConfigurationBuilder(
moderationProvider = GatewayModerationProvider(apiKey = GATEWAY_API_KEY),
)
},
)
},
onClose = onClose,
)
}

See Theming for more details.

Native Android Canvas#

CE.SDK supports rendering on two of the most popular native android views that allow GPU rendering: TextureView and SurfaceView:

import androidx.compose.runtime.Composable
import ly.img.editor.Editor
import ly.img.editor.configuration.contentmoderation.ContentModerationConfigurationBuilder
import ly.img.editor.configuration.contentmoderation.moderation.GATEWAY_API_KEY
import ly.img.editor.configuration.contentmoderation.moderation.GatewayModerationProvider
import ly.img.editor.core.configuration.EditorConfiguration
import ly.img.editor.core.configuration.remember
import ly.img.editor.core.engine.EngineRenderTarget
@Composable
fun EditorRenderTargetScreen(onClose: (error: Throwable?) -> Unit) {
Editor(
license = null, // pass null or empty for evaluation mode with watermark
engineRenderTarget = EngineRenderTarget.SURFACE_VIEW, // EngineRenderTarget.SURFACE_VIEW, EngineRenderTarget.TEXTURE_VIEW
configuration = {
EditorConfiguration.remember(
builderFactory = {
ContentModerationConfigurationBuilder(
moderationProvider = GatewayModerationProvider(apiKey = GATEWAY_API_KEY),
)
},
)
},
onClose = onClose,
)
}

Localization#

See Localization for supported languages, adding support to new languages and replacing existing keys.

UI Layout#

All the configurable components are located at starter-kit/src/main/kotlin/ly/img/editor/configuration/contentmoderation/component:

  • CanvasMenu.kt - see Canvas Menu for full configuration options.
  • Dock.kt - see Dock for full configuration options.
  • InspectorBar.kt - see Inspector Bar for full configuration options.
  • NavigationBar.kt - see Navigation Bar for full configuration options.
  • Overlay.kt - see Overlay for full configuration options.

Troubleshooting#

Editor doesn’t load#

  • Check onCreate: Ensure onCreate callback loads a scene and no coroutine is stuck infinitly
  • Verify the baseURL: Assets must be accessible from the CDN or your self-hosted location
  • Check logcat errors: Look for error in Android logcat

Moderation results don’t appear#

  • Set your API key: Paste your IMG.LY Gateway key into GATEWAY_API_KEY in GatewayModerationProvider.kt; without it every check reports a missing-key error
  • Credit the balance: Insufficient credits means the key is valid but the account has no Gateway balance—top up at img.ly/dashboard/credit-balance
  • Read the sheet’s error state: A rejected key, an empty balance or an unreachable service is reported there rather than as empty results
  • Check logcat errors: Look for error in Android logcat

Export fails or produces blank images#

  • Wait for content to load: Ensure images are fully loaded before exporting
  • Check logcat errors: Look for error in Android logcat

Watermark appears in production#


Next Steps#