Litert Lm
Android (Google)OfficialCreated by Google LLCDistributed by Android (Google)
- Skill
- Tool integration
- Data and AI
- Apache-2.0
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a comprehensive guide for developers to implement on-device LLM capabilities in Android applications using the LiteRT-LM Kotlin API. It covers the end-to-end process from configuring Gradle dependencies and Android manifest requirements to managing the engine lifecycle, handling model file storage, and building a reactive UI with Jetpack Compose. The skill emphasizes best practices for performance, such as off-main-thread initialization, single-thread executor management, and proper resource cleanup to ensure stable and responsive AI features.
What it does
- Configures Android projects for LiteRT-LM integration including Gradle and manifest settings.
- Provides patterns for background engine initialization to prevent UI blocking.
- Demonstrates how to implement a ViewModel for managing conversation state and streaming responses.
- Offers guidance on handling model files on the device filesystem.
- Includes boilerplate for a Jetpack Compose UI to interact with the LLM.
- Outlines lifecycle management to safely handle engine and conversation resources.
When to use it
- When you need to add chat, summarization, or text extraction features to an Android app.
- When you want to run open LLMs like Gemma, Qwen, Llama, or Phi locally on the device.
- When you require CPU or GPU-accelerated inference for text models.
- When you are building an app that needs to handle large model files outside of the APK.
Advantages
- Enables private, offline-capable AI features within Android applications.
- Provides structured guidance on avoiding common pitfalls like main-thread blocking.
- Includes specific instructions for GPU acceleration and dependency management.
- Promotes robust lifecycle management for resource-intensive AI engines.
Usage examples
- “How do I add the LiteRT-LM dependency to my Android project?”
- The agent provides the specific implementation line for build.gradle.kts and explains the necessary minSdk requirements.
- “Show me how to initialize the LiteRT-LM engine in a ViewModel.”
- The agent provides a code snippet demonstrating the use of a single-thread executor and EngineConfig to initialize the model off the main thread.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/android/litert-lm/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip litert-lm.zip -d .agents/skills
Documentation by Google LLC
This skill provides step-by-step guidance for building an Android app that runs
an open text LLM through the LiteRT-LM Kotlin API
(com.google.ai.edge.litertlm) on the CPU or the GPU. The model is one
.litertlm file. Images, audio, tool calling, and NPU backends aren't covered.
For vision and audio models with .tflite files, suggest the litert-runtime
skill.
When a LiteRT-LM class or method that this skill uses doesn't compile, you must read the LiteRT-LM Android guide and the LiteRT-LM README before changing the code.
Prerequisites
A Kotlin Android project (the Android Studio Empty Activity template is enough). The
litertlm-android0.18.0 AAR declaresminSdk24.The dependency in the app-level
build.gradle.ktsfile from Google Maven:implementation("com.google.ai.edge.litertlm:litertlm-android:0.18.0"). Google Maven lists the later releases.For the GPU backend, two
uses-native-libraryentries inside<application>inAndroidManifest.xml. Add theINTERNETpermission if the app downloads the model.The model file lives on the device's file system (hundreds of MB to a few GB), never in
assets/or in the APK.
Critical mandates
Dependency first : Before writing code that imports
com.google.ai.edge.litertlm, add thelitertlm-androiddependency from Step 1 if the project lacks it. Verify with./gradlew assembleDebug.Background initialization :
Engine(config).initialize()loads the weights and blocks for seconds. Call it on a background thread, never on the main thread.Single owner : One single-thread executor owns the
Engineand theConversation. Creation, streaming, cancellation, and closing run on it.Close once :
EngineandConversationareAutoCloseable. A secondcloseon either throwsIllegalStateException. So doescloseon an engine whoseinitializethrew. Store the engine only afterinitializereturns.Model outside the APK : Pass an absolute path to
EngineConfig. Never bundle a.litertlmfile inassets/.
Detailed steps
Step 1: Configure Gradle and the manifest
Search the project for libs.versions.toml, app/build.gradle.kts, and
AndroidManifest.xml before editing build files. Configure minSdk and add the
LiteRT-LM dependency in app/build.gradle.kts:
<br />
android {
defaultConfig {
minSdk = 24
}
}
dependencies {
implementation("com.google.ai.edge.litertlm:litertlm-android:0.18.0")
}If the project uses Hilt, check the Dagger version first. litertlm-android
0.18.0 carries Kotlin 2.4 metadata, which Hilt 2.59.2 can't read. The build
then fails in hiltJavaCompileDebug with Provided Metadata instance has version 2.4.0, while maximum supported version is 2.3.0. Pick one fix:
Path A : Upgrade the Hilt version (
com.google.dagger) to 2.60.1 or later. Ask the user before editinggradle/libs.versions.toml.Path B : Keep the Hilt version and add a later version of the metadata reader to the annotation processor path in
app/build.gradle.kts:
<br />
dependencies {
ksp("org.jetbrains.kotlin:kotlin-metadata-jvm:2.4.21")
}For the GPU backend, add both lines inside <application> in
AndroidManifest.xml. Add the INTERNET permission outside <application>
when the app downloads the model:
<br />
<uses-permission android:name="android.permission.INTERNET"/>
<application>
<uses-native-library
android:name="libvndksupport.so"
android:required="false"/>
<uses-native-library
android:name="libOpenCL.so"
android:required="false"/>
</application>Step 2: Pick a model from litert-community
Choose a .litertlm file from litert-community on Hugging Face. Start with
litert-community/gemma-4-E2B-it-litert-lm (the file gemma-4-E2B-it.litertlm,
2.6 GB). For a 0.6 GB model, start with litert-community/Qwen3-0.6B (the file
Qwen3-0.6B.litertlm). Both files run on the CPU and the GPU. A SoC suffix such
as _Google_Tensor_G5 or .mediatek.mt6993 marks an NPU build, which this
skill doesn't cover. The model card names the tested backends and the size.
You must read get a model before you choose a model file other than these
two, before you write the in-app download code, and when no .litertlm file
exists for the model.
Step 3: Put the model file on the device
Pick one path:
Path A (local testing with
adb) : Run the following commands only when all three are true:adb deviceslists a device, the debug build of the app is installed on it, and the user supplied the local.litertlmpath. If any of them is false, don't run the commands. Report them to the user as a post-implementation step instead. Replace<path>with the user's local path and<package>with the app's application ID:adb push <path> /data/local/tmp/model.litertlmadb shell run-as <package> cp /data/local/tmp/model.litertlm files/
Path B (in-app download) : Write the download code in the app. Use the app's HTTP client to write into
filesDir, orDownloadManagerto write intogetExternalFilesDir(null).DownloadManagercan't write to internal storage. Show the download progress and check the file size against the model card before loading. Never download a model file in the agent's own session.Path C (Play for On-Device AI) : Write the code that resolves the path of a fast-follow or on-demand AI pack with the AI Delivery library (
com.google.android.play:ai-delivery):AiPackManagerFactory.getInstance(context).getPackLocation(name)?.assetsPath(). The call returnsnulluntil the pack has finished downloading, so show a download state in that case. Creating the AI pack module and uploading the app to Play Console are post-implementation steps for the user.
On every path, pass the absolute path of the file to the engine.
Step 4: Initialize the engine off the main thread
Create the engine with EngineConfig(modelPath = ..., backend = ..., cacheDir = ...) and call initialize on a background thread:
Backend.CPU()is the default ofEngineConfigand needs nothing from the device.Backend.GPU()needs the two manifest lines from Step 1. Without them, the engine initializes, and the first reply ends in the errorCan not find OpenCL library on this device.cacheDirspeeds up the second load.
Step 5: Implement the conversation ViewModel
Drive the engine and the conversation from a single-thread executor inside an
AndroidViewModel, stream the reply, and expose immutable state to the UI:
<br />
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import com.google.ai.edge.litertlm.Backend
import com.google.ai.edge.litertlm.Contents
import com.google.ai.edge.litertlm.Conversation
import com.google.ai.edge.litertlm.ConversationConfig
import com.google.ai.edge.litertlm.Engine
import com.google.ai.edge.litertlm.EngineConfig
import java.util.concurrent.Executors
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
data class ChatState(
val ready: Boolean = false,
val busy: Boolean = false,
val reply: String = "",
val error: String? = null
)
class ChatViewModel(app: Application) : AndroidViewModel(app) {
private val executor = Executors.newSingleThreadExecutor()
private val scope = CoroutineScope(
SupervisorJob() + executor.asCoroutineDispatcher()
)
private val mutex = Mutex()
private var engine: Engine? = null
private var conversation: Conversation? = null
private val _state = MutableStateFlow(ChatState())
val state: StateFlow<ChatState> = _state.asStateFlow()
fun load(modelPath: String, backend: Backend = Backend.CPU()) {
scope.launch {
mutex.withLock {
if (engine?.engineConfig?.modelPath == modelPath) return@launch
release()
try {
val config = EngineConfig(
modelPath = modelPath,
backend = backend,
cacheDir = getApplication<Application>().cacheDir.path
)
engine = Engine(config).also { it.initialize() }
conversation = checkNotNull(engine).createConversation(
ConversationConfig(
systemInstruction =
Contents.of("You are a helpful assistant.")
)
)
_state.value = ChatState(ready = true)
} catch (e: Exception) {
release()
_state.value = ChatState(error = e.message)
}
}
}
}
fun send(text: String) {
scope.launch {
if (!_state.value.ready || _state.value.busy) return@launch
_state.update { it.copy(busy = true, reply = "", error = null) }
try {
val replies = checkNotNull(conversation).sendMessageAsync(text)
replies.collect { message ->
_state.update { it.copy(reply = it.reply + message) }
}
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
_state.update { it.copy(error = e.message) }
} finally {
_state.update { it.copy(busy = false) }
}
}
}
private suspend fun release() {
_state.update { it.copy(ready = false) }
while (_state.value.busy) {
conversation?.cancelProcess()
delay(100)
}
conversation?.close()
conversation = null
engine?.close()
engine = null
_state.value = ChatState()
}
override fun onCleared() {
scope.launch {
mutex.withLock { release() }
executor.shutdown()
}.invokeOnCompletion { scope.cancel() }
}
}sendMessageAsync(text) returns a Flow<Message> of chunks, and toString
gives a chunk's text. sendMessage(text) blocks and returns the whole reply.
The conversation keeps its history, so the next send continues the chat.
busy keeps the button disabled until the flow completes.
To set the sampling, pass SamplerConfig(topK = 10, topP = 0.95, temperature = 0.8) in ConversationConfig. All three parameters are required,
and topP and temperature are Double.
load with the model path that is already loaded does nothing, whatever
backend it asks for. A second call after a rotation keeps the loaded model,
also after the app has fallen back to another backend. send runs only when
the state is ready and not busy, the same test as the button. A tap that
lands while release stops a reply does nothing. A tap queued behind a
load reaches the model that load loaded when no reply was streaming.
Step 6: Wire the Compose screen
viewModel and collectAsStateWithLifecycle come from
androidx.lifecycle:lifecycle-viewmodel-compose and
androidx.lifecycle:lifecycle-runtime-compose (2.10.0):
<br />
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.ai.edge.litertlm.Backend
@Composable
fun ChatScreen(modelPath: String, viewModel: ChatViewModel = viewModel()) {
val state by viewModel.state.collectAsStateWithLifecycle()
var input by remember { mutableStateOf("") }
LaunchedEffect(modelPath) { viewModel.load(modelPath, Backend.GPU()) }
Column {
Text(
state.reply,
Modifier.weight(1f).verticalScroll(rememberScrollState())
)
state.error?.let { Text(it) }
TextField(value = input, onValueChange = { input = it })
Button(
onClick = { viewModel.send(input); input = "" },
enabled = state.ready && !state.busy
) {
Text("Send")
}
}
}Step 7: Manage the engine lifecycle
Keep the engine in the
ViewModelfor as long as its screen lives. When several screens share the engine, keep it in an application-scoped holder.The
ViewModelkeeps its own thread and scope.viewModelScopeis cancelled beforeonClearedruns, so a close launched there wouldn't run.onClearedcancelsscopeonce the close is done.releaseturnsreadyoff and callscancelProcessuntil the streaming reply's flow has ended. The flow ends at the latest when the reply ends on its own.releasethen closes the conversation and the engine, each once.onClearedand aloadthat replaces the model both callreleaseunder theMutex, one at a time, becausereleasesuspends while a reply stops.To start a fresh chat, close the conversation and create a new one with
createConversation. Keep the engine loaded.
Post-implementation steps for the user
Report the steps that apply to the user after the code compiles:
When Path A in Step 3 didn't run, copy the model to the device with the two
adbcommands from Path A.When the app uses Path C in Step 3, create the AI pack module that holds the
.litertlmfile and upload the app to Play Console.When no
.litertlmfile exists for the model, convert the model on a workstation. This skill doesn't convert models.When the user asks for a complete reference app, point them to Google AI Edge Gallery.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| First reply is slow. | initialize ran on first use. | Load at app start and set cacheDir. |
First GPU reply ends in Can not find OpenCL library on this device. | The manifest lacks the uses-native-library entries. | Add both entries from Step 1 inside <application>. |
close throws IllegalStateException. | The engine or conversation was closed twice, or initialize threw. | Store the engine only after initialize returns, and set each field to null after close. |
Build fails with [Hilt] Provided Metadata instance has version 2.4.0. | Hilt 2.59.2 can't read the Kotlin 2.4 metadata in litertlm-android 0.18.0. | Follow Path A or Path B in Step 1. |
Checklist
[ ] Is
minSdkset to24or higher and iscom.google.ai.edge.litertlm:litertlm-android:0.18.0added inapp/build.gradle.kts?[ ] For the GPU backend, does
AndroidManifest.xmldeclarelibvndksupport.soandlibOpenCL.sowithandroid:required="false"inside<application>?[ ] Is the
.litertlmfile loaded from an absolute path on the device's file system, never fromassets/?[ ] Do
initialize,sendMessageAsync,cancelProcess, andcloseall run on the same single-thread background executor?[ ] Is the conversation closed before the engine, each exactly once?
[ ] Does the project compile with
./gradlew assembleDebug?
Antipatterns
Never call
Engine.initializeon the Android main thread.Never place a
.litertlmfile inassets/or ship it inside the APK.Never call
closeon an engine whoseinitializethrew.Never close the engine while a reply is streaming; call
cancelProcessuntil the flow ends first.Never close
EngineorConversationfromviewModelScopeinonCleared; that scope is already cancelled.
Files
- ATTRIBUTION.mdAdded by AutoGineer958 B
- LICENSELicence file added by AutoGineer11.1 KiB
- SKILL.md17.9 KiB
- references/android/google/play/on-device-ai.md36.8 KiB
- references/get-a-model.md1.8 KiB
references/android/google/play/on-device-ai.md
Introduction
Play for On-device AI brings the benefits of Android App Bundles and Google Play delivery to custom ML model distribution so you can improve model performance with less device ecosystem complexity at no additional cost. It lets you publish a single artifact to Play containing your code, assets, and ML models and to choose from a number of delivery modes and targeting options.
Benefits
Upload a single publishing artifact to Google Play and delegate hosting, delivery, updates, and targeting to Play at no additional cost.
Deliver your ML models at install-time, fast-follow, or on-demand.
Install-time delivery can guarantee that a very large model is present when your app is opened. Your model will be installed as an APK.
Fast-follow delivery occurs automatically in the background after your app has been installed. Users may open your app before your model has been fully downloaded. Your model will be downloaded to your app's internal storage space.
On-demand delivery lets you request the model at runtime, which is useful if the model is only required for certain user-flows. Your model will be downloaded to your app's internal storage space.
Deliver variants of your ML models that are targeted to specific devices based on device model, system properties, or RAM.
Keep app updates small and optimized with Play's automatic patching, which means only the differences in files need to be downloaded.
Considerations
By using Play for On-device AI you agree to the terms in the Google Play Developer Distribution Agreement and the Play Core Software Development Kit Terms of Service.
Models downloaded by Play for On-device AI should only be used by your apps. Models shouldn't be offered to other apps.
Individual AI packs can be up to 1.5GB, based on their compressed download sizes. The maximum cumulative app size of any version of your app generated from your app bundle is 4GB.
Apps over 1GB in size must set min SDK Level to 21 or higher.
How to use Play for On-device AI
Play for On-device AI uses AI packs. You package custom models that are ready for distribution in AI packs in your app bundle. You can choose whether the AI pack should be delivered at install-time, fast-follow, or on-demand.
By packaging AI packs with your app bundle, you can use all of Play's existing testing and release tools, such as test tracks and staged rollouts to manage your app's distribution with your custom models.
AI packs are updated together with the app binary. If your new app release doesn't make changes to an AI pack, then Play's automatic patching process will ensure the user doesn't have to re-download it. Play will just download what's changed when it updates the app.
AI packs only contain models. Java/Kotlin and native libraries are not allowed. If you need to ship libraries or code to run your ML model, move it into the base module or a feature module. You can configure your feature module so that it has the same download and targeting settings as the AI pack.
[!NOTE] Note: AI packs don't contain Java/Kotlin or native libraries. You can ship libraries or code to run your ML models in feature modules.
Use LiteRT and MediaPipe with AI packs
You can use LiteRT and MediaPipe with AI packs. Package your model in an AI pack and then access it using the instructions for install-time packs or for fast-follow and on-demand packs.
Further reading:
The sample app shows how you can package a LiteRT model in an AI pack and load it at runtime.
There are many pretrained LiteRT models that you can use in AI packs to get started.
Getting started with MediaPipe
For fast-follow and on-demand packs, you can use AssetCache.java to load your assets (eg.
.binarypbfiles) by their file paths.For install-time packs, you can use AndroidAssetUtil.java.
Get started with AI packs
At a high level, here's how you can start using Play for On-device AI:
Package your models into AI packs into your Android App Bundle and specify how the AI packs should be delivered.
[Optional] If you want to deliver different models to different devices, you can configure device targeting for your AI packs. For example, you could deliver AI pack A to a specific device model, AI pack B to devices with at least 6GB of RAM, and all other devices could receive no model.
[Optional] If you're using on-demand or fast-follow delivery, integrate the Play AI Delivery Library into your app to download your AI packs as needed.
Test and release your app bundle to Google Play.
Check Android Gradle Plugin version
To use AI packs, ensure that your Android Gradle Plugin (AGP) version is at least 8.8. This version is packaged with Android Studio Ladybug 2.
Extract your model into an AI pack
Android Studio is not required for the following steps.
In the top-level directory of your project, create a directory for the AI pack. This directory name is used as the AI pack name. AI pack names must start with a letter and can only contain letters, numbers, and underscores.
In the AI pack directory, create a
build.gradlefile and add the following code. Make sure to specify the name of the AI pack and only one delivery type:// In the AI pack's build.gradle file: plugins { id 'com.android.ai-pack' } aiPack { packName = "ai-pack-name" // Directory name for the AI pack dynamicDelivery { deliveryType = "[ install-time | fast-follow | on-demand ]" } }In the project's app
build.gradlefile, add the name of every AI pack in your project as shown below:// In the app build.gradle file: android { ... assetPacks = [":ai-pack-name", ":ai-pack2-name"] }In the project's
settings.gradlefile, include all AI packs in your project as shown below:// In the settings.gradle file: include ':app' include ':ai-pack-name' include ':ai-pack2-name'Inside your AI pack, create a
src/main/assets/directory.Place your models in the
src/main/assetsdirectory. You can create subdirectories in here as well. The directory structure for your app should now look like the following:build.gradlesettings.gradleapp/ai-pack-name/build.gradleai-pack-name/src/main/assets/your-model-directories
Add code to load and run your models. How you do this will depend on the delivery mode of your AI packs. See instructions for install-time and fast-follow/on-demand below.
[Optional] Configure device targeting to deliver different models to different devices.
Build the Android App Bundle with Gradle. In the generated app bundle, the root-level directory now includes the following:
ai-pack-name/manifest/AndroidManifest.xml: Configures the AI pack's identifier and delivery modeai-pack-name/assets/your-model-directories: Directory that contains all assets delivered as part of the AI pack
Gradle generates the manifest for each AI pack and outputs the
assets/directory for you.
Configure install-time delivery
AI packs configured as install-time are immediately available at app launch. Use the Java AssetManager API to access AI packs served in this mode:
import android.content.res.AssetManager;
...
Context context = createPackageContext("com.example.app", 0);
AssetManager assetManager = context.getAssets();
InputStream is = assetManager.open("model-name");Configure fast-follow and on-demand delivery
To download AI packs with fast-follow or on-demand delivery, use the Play AI Delivery Library.
Declare dependency on the Play AI Delivery Library
In your app's build.gradle file, declare a dependency on the Play AI Delivery
Library:
dependencies {
...
implementation "com.google.android.play:ai-delivery:0.1.1-alpha01"
}Check status
Each AI pack is stored in a separate folder in the app's internal storage. Use
the
getPackLocation()
method to determine the root folder of an AI pack. This method returns the
following values:
| Return value | Status |
|---|---|
A valid AiPackLocation object | AI pack root folder is ready for immediate access at assetsPath() |
null | Unknown AI pack or AI packs are not available |
[!NOTE] Note: Don't rely on cached AI pack locations between app launches. The app should always check for the existence of AI packs at every launch. AI packs may become invalid due to app updates or if the user clears the app data.
Get download information about AI packs
Use the
getPackStates()
method to determine the size of the download and whether the pack is already
downloading.
Task<AiPackStates> getPackStates(List<String> packNames)getPackStates() is an asynchronous method that returns a Task<AiPackStates>.
The
packStates()
method of an AiPackStates object returns a Map<String, AiPackState>. This
map contains the state of each requested AI pack, keyed by its name:
Map<String, AiPackState> AiPackStates#packStates()The final request is shown by the following:
final String aiPackName = "myAiPackName";
aiPackManager
.getPackStates(Collections.singletonList(aiPackName))
.addOnCompleteListener(new OnCompleteListener<AiPackStates>() {
@Override
public void onComplete(Task<AiPackStates> task) {
AiPackStates aiPackStates;
try {
aiPackStates = task.getResult();
AiPackState aiPackState =
aiPackStates.packStates().get(aiPackName);
} catch (RuntimeExecutionException e) {
Log.d("MainActivity", e.getMessage());
return;
});The following
AiPackState
methods provide the size of the AI pack, the downloaded amount so far (if
requested), and the amount already transferred to the app:
To get the status of an AI pack, use the
status()
method, which returns the status as an integer that corresponds to a constant
field in the
AiPackStatus
class. An AI pack that's not installed yet has the status
AiPackStatus.NOT_INSTALLED.
If a request fails, use the
errorCode()
method, whose return value corresponds to a constant field in the
AiPackErrorCode
class.
Install
Use the
fetch()
method to download an AI pack for the first time or call for the update of an AI
pack to complete:
Task<AiPackStates> fetch(List<String> packNames)This method returns an
AiPackStates
object containing a list of packs and their initial download states and sizes.
If an AI pack requested via fetch() is already downloading, the download
status is returned and no additional download is started.
[!NOTE] Note: In most cases, you implement a
listenerto track the download and installation process as covered in the next section.
Monitor download states
You should implement an
AiPackStateUpdateListener
to track the installation progress of AI packs. The status updates are broken
down per pack to support tracking the status of individual AI packs. You can
start using available AI packs before all other downloads for your request have
completed.
void registerListener(AiPackStateUpdateListener listener)
void unregisterListener(AiPackStateUpdateListener listener)[!NOTE] Note: The Play Store automatically triggers the download of any
fast-followpacks after the user installs or updates the app. However, these packs may not be ready to use immediately. You must check the status of thefast-followpacks at every app launch. If the download is in progress, monitor it with a listener. If the download is cancelled or paused, you can resume it by using thefetch()method, as covered in the Install section.
Large downloads
If the download is larger than 200 MB and the user is not on Wi-Fi, the download
does not start until the user explicitly gives their consent to proceed with the
download using a mobile data connection. Similarly, if the download is large and
the user loses Wi-Fi, the download is paused and explicit consent is required to
proceed using a mobile data connection. A paused pack has state
WAITING_FOR_WIFI. To trigger the UI flow to prompt the user for consent, use
the
showConfirmationDialog()
method.
Note that if the app does not call this method, the download is paused and will resume automatically only when the user is back on a Wi-Fi connection.
Required user confirmation
If a pack has the REQUIRES_USER_CONFIRMATION status, the download won't
proceed until the user accepts the dialog that is shown with
showConfirmationDialog().
This status can occur when the app is not recognized by Play---for example, if the
app was sideloaded. Note that calling
showConfirmationDialog()
in this case will cause the app to be updated. After the update, you will need
to request the AI packs again.
The following is an example implementation of a listener:
AiPackStateUpdateListener aiPackStateUpdateListener = new AiPackStateUpdateListener() {
private final ActivityResultLauncher<IntentSenderRequest> activityResultLauncher =
registerForActivityResult(
new ActivityResultContracts.StartIntentSenderForResult(),
new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
if (result.getResultCode() == RESULT_OK) {
Log.d(TAG, "Confirmation dialog has been accepted.");
} else if (result.getResultCode() == RESULT_CANCELED) {
Log.d(TAG, "Confirmation dialog has been denied by the user.");
}
}
});
@Override
public void onStateUpdate(AiPackState aiPackState) {
switch (aiPackState.status()) {
case AiPackStatus.PENDING:
Log.i(TAG, "Pending");
break;
case AiPackStatus.DOWNLOADING:
long downloaded = aiPackState.bytesDownloaded();
long totalSize = aiPackState.totalBytesToDownload();
double percent = 100.0 * downloaded / totalSize;
Log.i(TAG, "PercentDone=" + String.format("%.2f", percent));
break;
case AiPackStatus.TRANSFERRING:
// 100% downloaded and assets are being transferred.
// Notify user to wait until transfer is complete.
break;
case AiPackStatus.COMPLETED:
// AI pack is ready to use. Run the model.
break;
case AiPackStatus.FAILED:
// Request failed. Notify user.
Log.e(TAG, aiPackState.errorCode());
break;
case AiPackStatus.CANCELED:
// Request canceled. Notify user.
break;
case AiPackStatus.WAITING_FOR_WIFI:
case AiPackStatus.REQUIRES_USER_CONFIRMATION:
if (!confirmationDialogShown) {
aiPackManager.showConfirmationDialog(activityResultLauncher);
confirmationDialogShown = true;
}
break;
case AiPackStatus.NOT_INSTALLED:
// AI pack is not downloaded yet.
break;
case AiPackStatus.UNKNOWN:
Log.wtf(TAG, "AI pack status unknown")
break;
}
}
}Alternatively, you can use the
getPackStates()
method to get the status of current downloads.
AiPackStates
contains the download progress, download status, and any failure error codes.
Access AI packs
You can access an AI pack using file system calls after the download request
reaches the
COMPLETED
state. Use the
getPackLocation()
method to get the root folder of the AI pack.
AI packs are stored in the assets directory within the AI pack root directory.
You can get the path to the assets directory by using the convenience method
assetsPath().
Use the following method to get the path to a specific asset:
private String getAbsoluteAiAssetPath(String aiPack, String relativeAiAssetPath) {
AiPackLocation aiPackPath = aiPackManager.getPackLocation(aiPack);
if (aiPackPath == null) {
// AI pack is not ready
return null;
}
String aiAssetsFolderPath = aiPackPath.assetsPath();
// equivalent to: FilenameUtils.concat(aiPackPath.path(), "assets");
String aiAssetPath = FilenameUtils.concat(aiAssetsFolderPath, relativeAiAssetPath);
return aiAssetPath;
}Configure device targeting
You can follow the device targeting instructions to specify devices or groups of devices that should receive your AI packs.
Other Play AI Delivery API methods
The following are some additional API methods you may want to use in your app.
Cancel request
Use
cancel()
to cancel an active AI pack request. Note that this request is a best-effort
operation.
Remove an AI pack
Use
removePack()
to schedule the removal of an AI pack.
Get locations of multiple AI packs
Use
getPackLocations()
to query the status of multiple AI packs in bulk, which returns a map of AI
packs and their locations. The map returned by getPackLocations() contains an
entry for each pack that is currently downloaded and up-to-date.
Device targeting
Device targeting gives you finer control over which parts of your app bundle are delivered to specific devices. For example, you could ensure that a large model is only delivered to devices with high RAM or you could deliver different versions of a model to different devices.
You can target device properties such as:
System on chip
Device model
Device RAM
Overview of required steps
The following steps are required to enable device targeting:
Define your device groups in an XML file.
Specify which parts of your bundle should go to which device groups.
[Optional] Test your configuration locally.
Upload your bundle (containing the XML file) to Google Play.
Check Android Gradle Plugin version
To use device targeting, ensure that your Android Gradle Plugin (AGP) version is at least 8.10.0. This is packaged with Android Studio (Meerkat 2 and later). Download the latest stable version of Android Studio.
Enable this feature in Android Gradle Plugin
Device targeting must be enabled explicitly in your gradle.properties file:
android.experimental.enableDeviceTargetingConfigApi=trueCreate a device targeting configuration XML file
The device targeting configuration file is an XML file in which you define your
custom device groups. For example, you could define a device group called
qti_v79 that contains all devices with the Qualcomm SM8750 system on chip:
<config:device-targeting-config
xmlns:config="http://schemas.android.com/apk/config">
<config:device-group name="qti_v79">
<config:device-selector>
<config:system-on-chip manufacturer="QTI" model="SM8750"/>
</config:device-selector>
</config:device-group>
</config:device-targeting-config>A device group is composed of up to 5 device selectors. A device is included in a device group if it satisfies any of its device selectors.
A device selector can have one or more device properties. A device is selected if it matches all of the selector's device properties.
If a device matches multiple groups, it will be served the content for the group that is defined first in the XML file. The order you define groups in the XML file is your priority order.
If a device matches no groups, it will receive the default "other" group. This group is automatically generated and shouldn't be defined explicitly.
Available device properties
device_ram : Device RAM requirements
min_bytes (inclusive): Minimum required RAM (in bytes)
max_bytes (exclusive): Maximum required RAM (in bytes)
included_device_ids : Device models to be included in this selector (max of 10000 device_ids per group) . This property is satisfied if the device matches any device_id in the list.
build_brand: Device manufacturer
build_device: Device model code
excluded_device_ids : Device models to be excluded in this selector (max of 10000 device_ids per group) . This property is satisfied if the device matches no device_id in the list.
build_brand: Device manufacturer
build_device: Device model code
required_system_features : Features that a device needs to have to be included by this selector (max of 100 features per group). A device needs to have all system features in this list to satisfy this property.
System feature reference
name: A system feature
forbidden_system_features : Features that a device mustn't have to be included by this selector (max of 100 features per group). If a device has any of the system features in this list it doesn't satisfy this property.
System feature reference
name: A system feature
system-on-chip: System on chips to be included in this selector. A device needs to have any chip in this list to satisfy this property.
manufacturer : System on chip manufacturer
model : System on chip model
[!TIP] Tip: Including multiple properties in a single selector creates a logical AND, for example:
<config:device-selector ram-min-bytes="7000000000"> <config:included-device-id brand="google" device="flame"/> </config:device-selector>will create the condition for all devices with > 7GB of RAM AND it is a Pixel 4 - also written as follows:
If you want an OR condition, create separate selectors in a single device group, for example:
<config:device-selector ram-min-bytes="7000000000"/> <config:device-selector> <config:included-device-id brand="google" device="flame"/> </config:device-selector>will create the condition for all devices with > 7GB of RAM OR it is a Pixel 4 - also written as follows:
Here is an example showing all possible device properties:
<config:device-targeting-config
xmlns:config="http://schemas.android.com/apk/config">
<config:device-group name="myCustomGroup1">
<config:device-selector ram-min-bytes="8000000000">
<config:included-device-id brand="google" device="redfin"/>
<config:included-device-id brand="google" device="sailfish"/>
<config:included-device-id brand="good-brand"/>
<config:excluded-device-id brand="google" device="caiman"/>
<config:system-on-chip manufacturer="Sinclair" model="ZX80"/>
<config:system-on-chip manufacturer="Commodore" model="C64"/>
</config:device-selector>
<config:device-selector ram-min-bytes="16000000000"/>
</config:device-group>
<config:device-group name="myCustomGroup2">
<config:device-selector ram-min-bytes="4000000000" ram-max-bytes="8000000000">
<config:required-system-feature name="android.hardware.bluetooth"/>
<config:required-system-feature name="android.hardware.location"/>
<config:forbidden-system-feature name="android.hardware.camera"/>
<config:forbidden-system-feature name="mindcontrol.laser"/>
</config:device-selector>
</config:device-group>
</config:device-targeting-config>Official device manufacturer and device model codes
You can find the correct formatting for the device manufacturer and model code by using the Device Catalog on the Google Play Console, by either:
Inspecting individual devices using the Device Catalog, and finding the manufacturer and model code in the locations as shown in the example below (For a Google Pixel 4a, the manufacturer is "Google" and the model code is "sunfish")'
Downloading a CSV of supported devices, and using the Manufacturer and Model Code for the build_brand and build_device fields, respectively.
Include your device targeting configuration file in your app bundle
Add the following to your main module's build.gradle file:
android {
...
bundle {
deviceTargetingConfig = file('device_targeting_config.xml')
deviceGroup {
enableSplit = true // split bundle by #group
defaultGroup = "other" // group used for standalone APKs
}
}
...
}device_targeting_config.xml is the path of your configuration file relative to
the main module. This ensures that your configuration file is packaged with your
app bundle.
The deviceGroup clause ensures that the APKs generated from your bundle are
split by device groups.
Use device targeting for your AI packs
You can keep size optimized on devices by only delivering your large models to devices that can run them.
Subdivide your AI packs by device groups by taking the existing AI pack directories created in the last step, and post-fixing the appropriate folders (as described below) with #group_myCustomGroup1, #group_myCustomGroup2, etc. When using the AI packs in your app, you won't need to address folders by postfix (in other words, the postfix is automatically stripped during the build process).
After the previous step, this might look like:
...
.../ai-pack-name/src/main/assets/image-classifier#group_myCustomGroup1/
.../ai-pack-name/src/main/assets/image-classifier#group_myCustomGroup2/
...In this example, you would reference ai-pack-name/assets/image-classifier/
without any postfixes.
Devices in myCustomGroup1 will receive all the assets under
image-classifier#group_myCustomGroup1/, while devices in myCustomGroup2 will
receive all the assets under image-classifier#group_myCustomGroup2/.
Devices that don't belong to either myCustomGroup1 or myCustomGroup2 will
receive an empty ai-pack-name pack.
This is because devices that don't match any device group will receive the
default variant of your AI pack. This includes anything that is not inside a
directory with a #group_suffix.
Once you have downloaded the AI pack, you can check whether your model is present by using the AssetManager for install-time packs or the AiPackManager for fast-follow and on-demand packs. Examples for doing this are shown for all delivery modes in the sample app.
[!IMPORTANT] Important: It's not possible to prevent any variant of your AI pack being delivered to certain devices. Non-targeted devices will always receive the default variant.
Use device targeting for your feature modules
You can also use device targeting for feature modules. Instead of subdividing feature modules by device group, you specify whether the entire module should be delivered based on device group membership.
To deliver a feature module to devices that belong to either myCustomGroup1 or
myCustomGroup2, modify its AndroidManifest.xml:
<manifest ...>
...
<dist:module dist:title="...">
<dist:delivery>
<dist:install-time>
<dist:conditions>
<dist:device-groups>
<dist:device-group dist:name="myCustomGroup1"/>
<dist:device-group dist:name="myCustomGroup2"/>
</dist:device-groups>
...
</dist:conditions>
</dist:install-time>
</dist:delivery>
</dist:module>
...
</manifest>[!NOTE] Note: Devices that are not targeted won't receive the feature module at all. This is different from targeting for AI packs, where devices that are not targeted receive a default, empty variant of the AI pack.
Test locally
Before creating a release for your new bundle, you can test locally with either Internal App Sharing or Bundletool.
Internal App Sharing
Internal App Sharing lets you use an app bundle to quickly generate a URL that you can tap on a local device to install exactly what Google Play would install for that device if that version of the app was live in a test or prod track.
Take a look at the internal app sharing instructions.
Bundletool
Alternatively, you can generate APKs using
bundletool (1.18.0 or above) and sideload
them onto your device. Follow these steps to test your app locally using
bundletool:
Build your app bundle with Android Studio or bundletool.
Generate APKs with the
--local-testingflag:java -jar bundletool-all.jar build-apks --bundle=path/to/your/bundle.aab \ --output=output.apks --local-testingConnect a device and run
bundletoolto sideload the APKs:# Example without Device Targeting Configuration java -jar bundletool.jar install-apks --apks=output.apks# Example with Device Targeting Configuration (you must specify which groups the connected device belongs to) java -jar bundletool.jar install-apks --apks=output.apks --device-groups=myCustomGroup1,myCustomGroup2
Limitations of local testing with bundletool
The following are limitations of local testing with bundletool:
fast-followpacks behave ason-demandpacks. That is, they won't be automatically fetched when the app is sideloaded. Developers need to request them manually when the app starts; this does not require any code changes in your app.Packs fetch from external storage instead of Play, so you cannot test how your code behaves in the case of network errors.
Local testing does not cover the wait-for-Wi-Fi scenario.
Updates are not supported. Before installing a new version of your build, manually uninstall the previous version.
Verify that the correct APKs are being installed
Use the following method to ensure only the correct APKs are installed on the device
adb shell pm path {packageName}You should see something like:
package:{...}/base.apk
package:{...}/split_config.en.apk
package:{...}/split_config.xxhdpi.apk
package:{...}/split_main_ai-pack-name.apk
package:{...}/split_main_ai-pack-name.config.group_myCustomGroup1.apkNote that you will only see APKs in this list, which are made from feature modules and install-time AI packs. On-demand and fast-follow AI packs are not installed as APKs.
Test and release on Google Play
We recommend that you test your app end to end on Google Play with an internal test track.
Once you've done this, you can incrementally release your app update to production with staged roll-outs.
Sample app using Play for On-device AI
Download the sample app.
It demonstrates how to use each of the delivery modes as well as the device targeting configuration. See the local testing section to get started.
Related content
Learn more about Android App Bundles and read the references for the AI Delivery SDK.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Google LLC
- Publisher
- Android (Google) (Official)
- Distributed by
- Android (Google)
- Source
- android/skills · device-ai/litert/litert-lm · commit bf68e2d
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE.txt)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Google LLC or Android (Google).
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download litert-lm.zip