# Add a new Launchable Education App **Intended audience**: this guide is for app developers who want to add their app to an Open Educational Experience Launcher. The launcher supports web technology based apps (HTML/Javascript) and Android native apps. Compatible apps use [OPDS 2.0](https://specs.opds.io/opds-2.0.html) to provide a collection of available learning units and [xAPI](https://xapi.com/overview/) to send data on lesson completion back to the launcher. You can find a full example collection of learning units at [https://demo.openeel.org](https://demo.openeel.org/). Terminology: * **Launchable Education App**: an educational app (e.g. math app, language app, assessment app) that can be launched from the apps list in the launcher. * **Learning Unit**: a distinct unit within a launchable app e.g. a lesson, assessment, etc. Each learning unit is represented by a [publication manifest](https://readium.org/webpub-manifest/). ## Step 1: Create an app manifest and OPDS collection for your app An Open Educational Experience Launcher gives students and teachers a way to browse a different apps, launch them, browse learning units within them, and make/share collections of learning units. **A) Create a launchable app manifest:** Example: ```json { "metadata": { "@type": "https://id.openeel.org/schema/launchable-app", "title": "Demo Launchable App", "author": { "name": "Ustad Mobile FZ-LLC", "links": { "href": "https://www.ustadmobile.com/" } }, "identifier": "https://demo.openeel.org/app", "language": "en", "modified": "2025-09-29T17:00:00Z" }, "links": [ { "rel": "self", "href": "https://demo.openeel.org/launchable-app.json", "type": "application/opds-publication+json" }, { "rel": "collection", "href": "https://demo.openeel.org/default-collection.json", "type": "application/opds+json" }, { "rel": "https://id.openeel.org/rel/app-launch-uri", "href": "https://demo.openeel.org/" }, { "rel": "https://id.openeel.org/rel/appstore-android", "href": "https://play.google.com/store/apps/details?id=demo.openeel.org", "title": "Get it on Google Play" }, { "rel": "terms-of-service", "href": "https://demo.openeel.org/terms-privacy.html" }, { "rel": "license", "href": "http://opensource.org/licenses/MIT" } ], "images": [ { "href": "https://demo.openeel.org/app-icon.png", "type": "image/png" } ] } ``` For a more options see [README_LAUNCHABLE_APP.md](../respect-lib-opds-model/README_LAUNCHABLE_APP.md). **B) Create a default collection of learning units:** Example (default-collection.json): ```json { "metadata": { "title": "Default Lesson collection" }, "links": [ {"rel": "self", "href": "https://demo.openeel.org/default-collection.json", "type": "application/opds+json"} ], "publications": [ { "metadata": { "@type": "http://schema.org/Game", "title": "Native Demo 001", "author": "Mullah Nasruddin", "identifier": "https://demo.openeel.org/grade/1/learningunits/1/", "language": "en", "modified": "2015-09-29T17:00:00Z", "subject": [ { "name": "Mathematics", "scheme": "https://www.bisg.org/#bisac", "code": "MAT000000" } ] }, "links": [ {"rel": "self", "href": "Lesson-manifest.json", "type": "application/opds-publication+json"}, { "rel": "https://id.openeel.org/rel/tincanxml", "href": "tincan.xml", "type": "application/xml" }, { "rel": "https://id.openeel.org/rel/launchable-app", "href": "https://demo.openeel.org/launchable-app.json", "type": "application/opds-publication+json" } ], "images": [ {"href": "Lesson-cover.png", "type": "image/png" } ] } ] } ``` **C) Create/generate a manifest for each learning unit** Example (Lesson-manifest.json) ```json { "metadata": { "@type": "http://schema.org/Game", "title": "Lesson 001", "author": "Mullah Nasruddin", "identifier": "https://demo.openeel.org/grade/1/learningunits/1/", "language": "en", "modified": "2015-09-29T17:00:00Z" }, "links": [ { "rel": "self", "href": "https://demo.openeel.org/grade/1/learningunits/1/manifest.json", "type": "application/opds-publication+json" }, { "rel": "https://id.openeel.org/rel/tincanxml", "href": "tincan.xml", "type": "application/xml" }, { "rel": "https://id.openeel.org/rel/launchable-app", "href": "https://demo.openeel.org/launchable-app.json", "type": "application/opds-publication+json" } ], "readingOrder": [ { "href": "https://demo.openeel.org/grade/1/learningunits/1/learningunit.html", "type": "text/html" } ], "images": [ { "href": "cover.png", "type": "image/png" } ], "resources": [ { "href": "audio.ogg", "type": "audio/ogg" }, { "href": "video.mp4", "type": "video/mp4" } ] } ``` Notes: * The ```resources``` section SHOULD list all resources required for the lesson to function offline such that they can be downloaded for offline use. **D) Create a tincan.xml file for each learning unit** Example: ```xml Lesson 1 A demo lesson learningunit.html ``` The tincan.xml file is used as per the [Rustici Launch Method](https://github.com/RusticiSoftware/launch/blob/master/lms_lrs.md) so learning units can send xAPI usage data back to the launcher. ## Step 2: Send xAPI usage data back to the launcher (HTML/Javascript apps) Update your HTML/Javascript to check for the presence of the xAPI launch parameters and send data back to the specified endpoint. This can be done using the [xAPI.js library](https://www.xapijs.dev/). This step can be skipped if your app is used exclusively as a native app. e.g. ```javascript //Downloaded as per https://github.com/xapijs/xapi/blob/develop/example/browser/module/index.html import XAPI from "./xapi_module.js"; const { endpoint, auth } = XAPI.getTinCanLaunchData(); //As per https://www.xapijs.dev/xapi-wrapper-library/helpers const queryParamsObject = XAPI.getSearchQueryParamsAsObject(location.search); const searchParams = new URLSearchParams(location.search); const xapi = new XAPI({ endpoint: endpoint, auth: auth }); function onLessonPassed() { const myStatement = { actor: queryParamsObject.actor, verb: { id: "http://adlnet.gov/expapi/verbs/passed", }, object: { id: searchParams.get("activity_id") }, result: { completion: true, success: true, score: { scaled: 1.0 } } }; console.log("XapiAuJs: Assignable unit: sending statement "); xapi.sendStatement({ statement: myStatement }).then((result) => { console.log(result); }).catch((error) => { const errorStr = JSON.stringify(error.toJSON(), null, 2); console.log(errorStr); }); } ``` # Step 3: Send xAPI usage data back to the launcher (Native Android apps) Update your app to use the xAPI-IPC library to send xAPI usage data back to the launcher: ```kotlin @Composable fun LessonScreen( modifier: Modifier = Modifier, lesson: LessonDestination ) { val context = LocalContext.current.getActivityContext() val json = remember { Json { encodeDefaults = false } } val ipcPackage = lesson.xapiIpcPackage val endpointUrl = lesson.endpoint?.let { Url(it) } val auth = lesson.auth val client = remember(ipcPackage, endpointUrl, auth) { if (ipcPackage != null && endpointUrl != null && auth != null) { XapiIpcClientBuilder(context, endpointUrl.toString()) .setAuth(auth) .setJson(json) .setIpcServicePackageName(ipcPackage) .build() } else { null } } val actorObject = remember(lesson.actor) { lesson.actor?.let { json.decodeFromString(XapiAgent.serializer(), it) } } val activityIdVal = lesson.activity_id fun onLessonPassed() { if(actorObject != null && activityIdVal != null && scoreFloat != null && client != null) { val result = client.statements.post( listOf( XapiStatement( actor = actorObject, verb = XapiVerb(id = "http://adlnet.gov/expapi/verbs/passed"), `object` = XapiActivity(id = activityIdVal), result = XapiResult( completion = true, success = true, score = XapiResult.Score( scaled = 1.0f ) ) ) ) ) resultStmtText = result.prettyResultString() }else { resultStmtText = "Could not send stmt: missing params" } } } ``` See the full native demo app (including library dependencies to add) [on GitHub](https://www.github.com/UstadMobile/DemoLaunchableApp/). # Step 4: Try adding your app on the launcher Login to the RESPECT launcher as an admin, go the Apps list, click add, then enter the URL for your launchable app manifest from step 1. You should now be able to browse learning units from the apps collections, launch learning units, and receive usage data using xAPI.